diff options
author | Andy Schwerin <schwerin@10gen.com> | 2012-05-30 17:48:52 -0400 |
---|---|---|
committer | Andy Schwerin <schwerin@10gen.com> | 2012-06-04 13:08:27 -0400 |
commit | c04922ad14ebe43d99ba056b7b3ae79860b8cd91 (patch) | |
tree | 5057ff729113f61028ed4c94ae96c26796d49c92 /src | |
parent | 7cb8e1f11959d8cb45954fcb1cfe13cdc6caa169 (diff) | |
download | mongo-c04922ad14ebe43d99ba056b7b3ae79860b8cd91.tar.gz |
SERVER-5702: C++ unit test framework that can be compiled separately.
Includes a unit test of the unit test framework written in the unit test
framework, itself.
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/dbtests/framework.cpp | 23 | ||||
-rw-r--r-- | src/mongo/unittest/SConscript | 12 | ||||
-rw-r--r-- | src/mongo/unittest/crutch.cpp | 34 | ||||
-rw-r--r-- | src/mongo/unittest/unittest-inl.h | 38 | ||||
-rw-r--r-- | src/mongo/unittest/unittest.cpp | 153 | ||||
-rw-r--r-- | src/mongo/unittest/unittest.h | 373 | ||||
-rw-r--r-- | src/mongo/unittest/unittest_main.cpp | 12 | ||||
-rw-r--r-- | src/mongo/unittest/unittest_test.cpp | 65 |
8 files changed, 534 insertions, 176 deletions
diff --git a/src/mongo/dbtests/framework.cpp b/src/mongo/dbtests/framework.cpp index 975212430b8..9ce7551b2bf 100644 --- a/src/mongo/dbtests/framework.cpp +++ b/src/mongo/dbtests/framework.cpp @@ -34,6 +34,7 @@ #include "mongo/db/instance.h" #include "mongo/unittest/unittest.h" #include "mongo/util/background.h" +#include "mongo/util/concurrency/mutex.h" #include "mongo/util/file_allocator.h" #include "mongo/util/version.h" @@ -49,6 +50,9 @@ namespace mongo { namespace dbtests { + mutex globalCurrentTestNameMutex("globalCurrentTestNameMutex"); + std::string globalCurrentTestName; + void show_help_text(const char* name, po::options_description options) { cout << "usage: " << name << " [options] [suite]..." << endl << options << "suite: run the specified test suite(s) only" << endl; @@ -60,13 +64,21 @@ namespace mongo { virtual void run(){ int minutesRunning = 0; - std::string lastRunningTestName = ::mongo::unittest::getExecutingTestName(); + std::string lastRunningTestName, currentTestName; + + { + scoped_lock lk( globalCurrentTestNameMutex ); + lastRunningTestName = globalCurrentTestName; + } while (true) { sleepsecs(60); minutesRunning++; - std::string currentTestName = ::mongo::unittest::getExecutingTestName(); + { + scoped_lock lk( globalCurrentTestNameMutex ); + currentTestName = globalCurrentTestName; + } if (currentTestName != lastRunningTestName) { minutesRunning = 0; @@ -251,6 +263,9 @@ namespace mongo { TestWatchDog twd; twd.go(); + // set tlogLevel to -1 to suppress tlog() output in a test program + tlogLevel = -1; + int ret = ::mongo::unittest::Suite::run(suites,filter); #if !defined(_WIN32) && !defined(__sunos__) @@ -267,3 +282,7 @@ namespace mongo { } // namespace mongo +void mongo::unittest::onCurrentTestNameChange( const std::string &testName ) { + scoped_lock lk( mongo::dbtests::globalCurrentTestNameMutex ); + mongo::dbtests::globalCurrentTestName = testName; +} diff --git a/src/mongo/unittest/SConscript b/src/mongo/unittest/SConscript index 9f434723991..43a945fe7ad 100644 --- a/src/mongo/unittest/SConscript +++ b/src/mongo/unittest/SConscript @@ -2,4 +2,14 @@ Import("env") -env.StaticLibrary("unittest", ['unittest.cpp']) +env.StaticLibrary("unittest", ['unittest.cpp'], + LIBDEPS=['$BUILD_DIR/mongo/foundation']) + +env.StaticLibrary("unittest_main", ['unittest_main.cpp'], + LIBDEPS=['unittest']) + +env.StaticLibrary("unittest_crutch", ['crutch.cpp']) + +env.Alias('unittests', + env.Program('unittest_test', 'unittest_test.cpp', + LIBDEPS=['unittest_main', 'unittest_crutch']) diff --git a/src/mongo/unittest/crutch.cpp b/src/mongo/unittest/crutch.cpp new file mode 100644 index 00000000000..e17a5e5428c --- /dev/null +++ b/src/mongo/unittest/crutch.cpp @@ -0,0 +1,34 @@ +/** +* Copyright (C) 2012 10gen Inc. +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License, version 3, +* as published by the Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +/** + * This file should go away. It contains stubs of functions that were needed to link the unit test + * framework. As we refactor the system, the contents of this file should _ONLY_ shrink, and + * eventually it should contain nothing. + */ + +#include "mongo/pch.h" + +#include "mongo/db/lasterror.h" +#include "mongo/util/assert_util.h" +#include "mongo/util/goodies.h" + +namespace mongo { + bool inShutdown() { return false; } + std::string getThreadName() { return "UNKNOWN"; } + void setLastError(int code, const char* msg) {} + bool StaticObserver::_destroyingStatics = false; +} // namespace mongo diff --git a/src/mongo/unittest/unittest-inl.h b/src/mongo/unittest/unittest-inl.h new file mode 100644 index 00000000000..86539d3ab1a --- /dev/null +++ b/src/mongo/unittest/unittest-inl.h @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2008 10gen Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +namespace mongo { + namespace unittest { + + template <typename T> + Test::RegistrationAgent<T>::RegistrationAgent(const std::string& suiteName, + const std::string& testName) { + Suite::getSuite(suiteName)->add<T>(testName); + } + + template<typename A, typename B> + std::string ComparisonAssertion::getComparisonFailureMessage(const std::string &theOperator, + const A &a, const B &b) { + std::ostringstream os; + os << "Expected " << _aexp << " " << theOperator << " " << _bexp + << " (" << a << " " << theOperator << " " << b << ")"; + return os.str(); + } + + } // namespace mongo +} // namespace unittest diff --git a/src/mongo/unittest/unittest.cpp b/src/mongo/unittest/unittest.cpp index 0844bd942da..34b90dccc33 100644 --- a/src/mongo/unittest/unittest.cpp +++ b/src/mongo/unittest/unittest.cpp @@ -1,5 +1,3 @@ -// mongo/unittest/unittest.cpp - /** * Copyright (C) 2008 10gen Inc. * @@ -16,25 +14,28 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include "mongo/pch.h" -#include "pch.h" #include "mongo/unittest/unittest.h" #include <iostream> #include <map> #include "mongo/util/assert_util.h" -#include "mongo/util/concurrency/mutex.h" +#include "mongo/util/log.h" namespace mongo { namespace unittest { namespace { - std::map<std::string, Suite *> *_suites = 0; + typedef std::map<std::string, Suite*> SuiteMap; + + inline SuiteMap &_allSuites() { + static SuiteMap allSuites; + return allSuites; + } - mutex currentTestNameMutex("currentTestNameMutex"); - std::string currentTestName; } // namespace class Result { @@ -72,9 +73,17 @@ namespace mongo { Result *Result::cur = 0; - TestCase::~TestCase() {} - void TestCase::setUp() {} - void TestCase::tearDown() {} + Test::Test() {} + Test::~Test() {} + + void Test::run() { + setUp(); + _doTest(); + tearDown(); + } + + void Test::setUp() {} + void Test::tearDown() {} Suite::Suite( const std::string &name ) : _name( name ) { registerSuite( name , this ); @@ -82,9 +91,11 @@ namespace mongo { Suite::~Suite() {} + void Suite::add(const std::string& name, const TestFunction& testFn) { + _tests.push_back(new TestHolder(name, testFn)); + } + Result * Suite::run( const std::string& filter ) { - // set tlogLevel to -1 to suppress tlog() output in a test program - tlogLevel = -1; log(1) << "\t about to setupTests" << std::endl; setupTests(); @@ -93,11 +104,8 @@ namespace mongo { Result * r = new Result( _name ); Result::cur = r; - /* see note in SavedContext */ - //writelock lk(""); - - for ( std::vector<TestCase*>::iterator i=_tests.begin(); i!=_tests.end(); i++ ) { - TestCase * tc = *i; + for ( std::vector<TestHolder*>::iterator i=_tests.begin(); i!=_tests.end(); i++ ) { + TestHolder * tc = *i; if ( filter.size() && tc->getName().find( filter ) == std::string::npos ) { log(1) << "\t skipping test: " << tc->getName() << " because doesn't match filter" << std::endl; continue; @@ -107,12 +115,9 @@ namespace mongo { bool passes = false; - { - scoped_lock lk(currentTestNameMutex); - currentTestName = tc->getName(); - } + onCurrentTestNameChange( tc->getName() ); - log(1) << "\t going to run test: " << tc->getName() << std::endl; + std::cout << "\t going to run test: " << tc->getName() << std::endl; std::stringstream err; err << tc->getName() << "\t"; @@ -121,23 +126,22 @@ namespace mongo { tc->run(); passes = true; } - catch ( MyAssertionException * ae ) { - err << ae->ss.str(); - delete( ae ); + catch ( const TestAssertionFailureException & ae ) { + err << ae.toString(); } - catch ( std::exception& e ) { - err << " exception: " << e.what(); + catch ( const std::exception& e ) { + err << " std::exception: " << e.what() << " in test " << tc->getName(); } catch ( int x ) { - err << " caught int : " << x << std::endl; + err << " caught int " << x << " in test " << tc->getName(); } catch ( ... ) { - err << "unknown exception in test: " << tc->getName() << std::endl; + err << "unknown exception in test: " << tc->getName(); } if ( ! passes ) { std::string s = err.str(); - log() << "FAIL: " << s << std::endl; + std::cout << "FAIL: " << s << std::endl; r->_fails++; r->_messages.push_back( s ); } @@ -147,44 +151,48 @@ namespace mongo { r->_rc = 17; - { - scoped_lock lk(currentTestNameMutex); - currentTestName = ""; - } + onCurrentTestNameChange( "" ); - log(1) << "\t DONE running tests" << std::endl; + std::cout << "\t DONE running tests" << std::endl; return r; } int Suite::run( const std::vector<std::string> &suites , const std::string& filter ) { + + if (_allSuites().empty()) { + std::cout << "error: no suites registered."; + return 1; + } + for ( unsigned int i = 0; i < suites.size(); i++ ) { - if ( _suites->find( suites[i] ) == _suites->end() ) { + if ( _allSuites().count( suites[i] ) == 0 ) { std::cout << "invalid test suite [" << suites[i] << "], use --list to see valid names" << std::endl; - return -1; + return 1; } } std::vector<std::string> torun(suites); if ( torun.empty() ) { - for ( std::map<std::string,Suite*>::iterator i=_suites->begin() ; i!=_suites->end(); i++ ) + for ( SuiteMap::const_iterator i = _allSuites().begin(); + i !=_allSuites().end(); ++i ) { + torun.push_back( i->first ); + } } std::vector<Result*> results; for ( std::vector<std::string>::iterator i=torun.begin(); i!=torun.end(); i++ ) { std::string name = *i; - Suite * s = (*_suites)[name]; + Suite* s = _allSuites()[name]; fassert( 16145, s ); - log() << "going to run suite: " << name << std::endl; + std::cout << "going to run suite: " << name << std::endl; results.push_back( s->run( filter ) ); } - Logstream::get().flush(); - std::cout << "**************************************************" << std::endl; int rc = 0; @@ -215,53 +223,60 @@ namespace mongo { } void Suite::registerSuite( const std::string &name , Suite * s ) { - if ( ! _suites ) - _suites = new std::map<std::string,Suite*>(); - Suite*& m = (*_suites)[name]; + Suite*& m = _allSuites()[name]; fassert( 10162, ! m ); m = s; } - void assert_pass() { - Result::cur->_asserts++; + Suite* Suite::getSuite(const std::string& name) { + Suite* result = _allSuites()[name]; + if (!result) + result = new Suite(name); // Suites are self-registering. + return result; } - void assert_fail( const char * exp , const char * file , unsigned line ) { - Result::cur->_asserts++; + void Suite::setupTests() {} - MyAssertionException * e = new MyAssertionException(); - e->ss << "ASSERT FAILED! " << file << ":" << line << std::endl; - std::cout << e->ss.str() << std::endl; - throw e; + TestAssertionFailureDetails::TestAssertionFailureDetails( + const std::string &theFile, + unsigned theLine, + const std::string &theMessage ) + : file( theFile ), line( theLine ), message( theMessage ) { } - MyAssertionException * MyAsserts::getBase() { - MyAssertionException * e = new MyAssertionException(); - e->ss << _file << ":" << _line << " " << _aexp << " != " << _bexp << " "; - return e; + TestAssertionFailureException::TestAssertionFailureException( + const std::string &theFile, + unsigned theLine, + const std::string &theFailingExpression ) + : _details( new TestAssertionFailureDetails( theFile, theLine, theFailingExpression ) ) { } - void MyAsserts::printLocation() { - log() << _file << ":" << _line << " " << _aexp << " != " << _bexp << " "; + std::string TestAssertionFailureException::toString() const { + std::ostringstream os; + os << getMessage() << " @" << getFile() << ":" << getLine(); + return os.str(); } - void MyAsserts::_gotAssert() { - Result::cur->_asserts++; + TestAssertion::TestAssertion( const std::string &file, unsigned line ) + : _file( file ), _line( line ) { + + ++Result::cur->_asserts; } - std::string getExecutingTestName() { - scoped_lock lk(currentTestNameMutex); - return currentTestName; + TestAssertion::~TestAssertion() {} + + void TestAssertion::fail( const std::string &message ) const { + throw TestAssertionFailureException( _file, _line, message ); } + ComparisonAssertion::ComparisonAssertion( const std::string &aexp, const std::string &bexp, + const std::string &file, unsigned line ) + : TestAssertion( file, line ), _aexp( aexp ), _bexp( bexp ) {} + std::vector<std::string> getAllSuiteNames() { std::vector<std::string> result; - if (_suites) { - for ( std::map<std::string, Suite *>::const_iterator i = _suites->begin(); - i != _suites->end(); ++i ) { - + for (SuiteMap::const_iterator i = _allSuites().begin(); i != _allSuites().end(); ++i) { result.push_back(i->first); - } } return result; } diff --git a/src/mongo/unittest/unittest.h b/src/mongo/unittest/unittest.h index 28329d0308f..68a0699c66a 100644 --- a/src/mongo/unittest/unittest.h +++ b/src/mongo/unittest/unittest.h @@ -1,5 +1,3 @@ -// mongo/unittest/unittest.h - /** * Copyright (C) 2008 10gen Inc. * @@ -17,35 +15,106 @@ */ /* - - simple portable regression system + * A C++ unit testing framework. + * + * For examples of basic usage, see mongo/unittest/unittest_test.cpp. */ #include <sstream> #include <string> #include <vector> +#include <boost/bind.hpp> +#include <boost/function.hpp> #include <boost/noncopyable.hpp> #include <boost/scoped_ptr.hpp> +#include <boost/shared_ptr.hpp> #include "mongo/util/assert_util.h" -#include "mongo/util/log.h" +#include "mongo/util/mongoutils/str.h" -#define ASSERT_THROWS(a,b) \ - try { \ - a; \ - mongo::unittest::assert_fail( #a , __FILE__ , __LINE__ ); \ - } catch ( b& ){ \ - mongo::unittest::assert_pass(); \ - } +/** + * Fail unconditionally, reporting the given message. + */ +#define FAIL(MESSAGE) ::mongo::unittest::TestAssertion( __FILE__ , __LINE__ ).fail( (MESSAGE) ) +/** + * Fails unless "EXPRESSION" is true. + */ +#define ASSERT_TRUE(EXPRESSION) ::mongo::unittest::TestAssertion( __FILE__, __LINE__ ).failUnless( \ + (EXPRESSION), "Expected: " #EXPRESSION ) +#define ASSERT(EXPRESSION) ASSERT_TRUE(EXPRESSION) +/** + * Fails if "EXPRESSION" is true. + */ +#define ASSERT_FALSE(EXPRESSION) ::mongo::unittest::TestAssertion( __FILE__, __LINE__ ).failIf( \ + (EXPRESSION), "Expected: !(" #EXPRESSION ")" ) -#define ASSERT_EQUALS(a,b) mongo::unittest::MyAsserts( #a , #b , __FILE__ , __LINE__ ).ae( (a) , (b) ) -#define ASSERT_NOT_EQUALS(a,b) mongo::unittest::MyAsserts( #a , #b , __FILE__ , __LINE__ ).nae( (a) , (b) ) +/* + * Binary comparison assertions. + */ +#define ASSERT_EQUALS(a,b) _ASSERT_COMPARISON(Equal, a, b) +#define ASSERT_NOT_EQUALS(a,b) _ASSERT_COMPARISON(NotEqual, a, b) +#define ASSERT_LESS_THAN(a, b) _ASSERT_COMPARISON(LessThan, a, b) +#define ASSERT_NOT_LESS_THAN(a, b) _ASSERT_COMPARISON(NotLessThan, a, b) +#define ASSERT_GREATER_THAN(a, b) _ASSERT_COMPARISON(GreaterThan, a, b) +#define ASSERT_NOT_GREATER_THAN(a, b) _ASSERT_COMPARISON(NotGreaterThan, a, b) +#define ASSERT_LESS_THAN_OR_EQUALS(a, b) ASSERT_NOT_GREATER_THAN(a, b) +#define ASSERT_GREATER_THAN_OR_EQUALS(a, b) ASSERT_NOT_LESS_THAN(a, b) -#define ASSERT(x) (void)( (!(!(x))) ? mongo::unittest::assert_pass() : FAIL(x) ) -#define FAIL(x) mongo::unittest::assert_fail( #x , __FILE__ , __LINE__ ) +/** + * Binary comparison utility macro. Do not use directly. + */ +#define _ASSERT_COMPARISON(COMPARISON, a, b) mongo::unittest::ComparisonAssertion( \ + #a, #b , __FILE__ , __LINE__ ).assert##COMPARISON( (a), (b) ) + +/** + * Verify that the evaluation of "EXPRESSION" throws an exception of type EXCEPTION_TYPE. + * + * If "EXPRESSION" throws no exception, or one that is neither of type "EXCEPTION_TYPE" nor + * of a subtype of "EXCEPTION_TYPE", the test is considered a failure and further evaluation + * halts. + */ +#define ASSERT_THROWS(EXPRESSION, EXCEPTION_TYPE) \ + do { \ + bool threw = false; \ + ::mongo::unittest::TestAssertion _testAssertion( __FILE__, __LINE__ ); \ + try { \ + EXPRESSION; \ + } catch ( const EXCEPTION_TYPE & ) { threw = true; } \ + if (!threw) \ + _testAssertion.fail("Expected expression " #EXPRESSION \ + " to throw " #EXCEPTION_TYPE " but it threw nothing."); \ + } while( false ) + + +/** + * Construct a single test, named "TEST_NAME" within the test case "CASE_NAME". + * + * Usage: + * + * TEST(MyModuleTests, TestThatFooFailsOnErrors) { + * ASSERT_EQUALS(error_success, foo(invalidValue)); + * } + */ +#define TEST(CASE_NAME, TEST_NAME) \ + class _TEST_TYPE_NAME(CASE_NAME, TEST_NAME) : public ::mongo::unittest::Test { \ + private: \ + virtual void _doTest(); \ + \ + static const RegistrationAgent<_TEST_TYPE_NAME(CASE_NAME, TEST_NAME) > _agent; \ + }; \ + const ::mongo::unittest::Test::RegistrationAgent<_TEST_TYPE_NAME(CASE_NAME, TEST_NAME) > \ + _TEST_TYPE_NAME(CASE_NAME, TEST_NAME)::_agent(#CASE_NAME, #TEST_NAME); \ + void _TEST_TYPE_NAME(CASE_NAME, TEST_NAME)::_doTest() + +/** + * Macro to construct a type name for a test, from its "CASE_NAME" and "TEST_NAME". + * Do not use directly in test code. + */ +#define _TEST_TYPE_NAME(CASE_NAME, TEST_NAME) \ + UnitTest__##CASE_NAME##__##TEST_NAME namespace mongo { @@ -53,157 +122,251 @@ namespace mongo { class Result; - class TestCase { + /** + * Type representing the function composing a test. + */ + typedef boost::function<void (void)> TestFunction; + + /** + * Container holding a test function and its name. Suites + * contain lists of these. + */ + class TestHolder : private boost::noncopyable { public: - virtual ~TestCase(); - virtual void setUp(); - virtual void tearDown(); - virtual void run() = 0; - virtual std::string getName() = 0; + TestHolder(const std::string& name, const TestFunction& fn) + : _name(name), _fn(fn) {} + + ~TestHolder() {} + void run() const { _fn(); } + std::string getName() const { return _name; } + + private: + std::string _name; + TestFunction _fn; }; - class Test : public TestCase { + /** + * Base type for unit test fixtures. Also, the default fixture type used + * by the TEST() macro. + * + * TODO(schwerin): Implement a TEST_F macro that allows testers to specify + * different subclasses of Test to be used as the test fixture. These subclasses + * could then provide per-test set-up and tear-down code by overriding the + * setUp and tearDown methods. + */ + class Test : private boost::noncopyable { public: Test(); virtual ~Test(); - virtual std::string getName(); + + void run(); protected: - void setTestName( const std::string &name ); + /** + * Registration agent for adding tests to suites, used by TEST macro. + */ + template <typename T> + class RegistrationAgent : private boost::noncopyable { + public: + RegistrationAgent(const std::string& suiteName, const std::string& testName); + }; private: - std::string _name; - }; - - template< class T > - class TestHolderBase : public TestCase { - public: - TestHolderBase() {} - virtual ~TestHolderBase() {} - virtual void run() { - boost::scoped_ptr<T> t; - t.reset( create() ); - t->run(); - } - virtual T * create() = 0; - virtual std::string getName() { - return demangleName( typeid(T) ); - } - }; + /** + * Called on the test object before running the test. + */ + virtual void setUp(); - template< class T > - class TestHolder0 : public TestHolderBase<T> { - public: - virtual T * create() { - return new T(); - } - }; + /** + * Called on the test object after running the test. + */ + virtual void tearDown(); - template< class T , typename A > - class TestHolder1 : public TestHolderBase<T> { - public: - TestHolder1( const A& a ) : _a(a) {} - virtual T * create() { - return new T( _a ); - } - const A _a; + /** + * The test itself. + */ + virtual void _doTest() = 0; }; - class Suite { + /** + * Representation of a collection of tests. + * + * One suite is constructed for each "CASE_NAME" when using the TEST macro. + * Additionally, tests that are part of dbtests are manually assigned to suites + * by the programmer by overriding setupTests() in a subclass of Suite. This + * approach is deprecated. + */ + class Suite : private boost::noncopyable { public: Suite( const string &name ); virtual ~Suite(); template<class T> - void add() { - _tests.push_back( new TestHolder0<T>() ); - } + void add() { add<T>(demangleName(typeid(T))); } template<class T , typename A > void add( const A& a ) { - _tests.push_back( new TestHolder1<T,A>(a) ); + add(demangleName(typeid(T)), boost::bind(&Suite::runTestObjectWithArg<T, A>, a)); + } + + template<class T> + void add(const std::string& name) { + add(name, &Suite::runTestObject<T>); } + void add(const std::string& name, const TestFunction& testFn); + Result * run( const std::string& filter ); static int run( const std::vector<std::string> &suites , const std::string& filter ); + /** + * Get a suite with the given name, creating it if necessary. + * + * The implementation of this function must be safe to call during the global static + * initialization block before main() executes. + */ + static Suite *getSuite(const string& name); + protected: - virtual void setupTests() = 0; + virtual void setupTests(); private: - typedef std::vector<TestCase *> TestCaseList; + typedef std::vector<TestHolder *> TestHolderList; + + template <typename T> + static void runTestObject() { + T testObj; + testObj.run(); + } + + template <typename T, typename A> + static void runTestObjectWithArg(const A& a) { + T testObj(a); + testObj.run(); + } + std::string _name; - TestCaseList _tests; + TestHolderList _tests; bool _ran; void registerSuite( const std::string &name , Suite *s ); }; - void assert_pass(); - void assert_fail( const char * exp , const char * file , unsigned line ); + /** + * Collection of information about failed tests. Used in reporting + * failures. + */ + class TestAssertionFailureDetails : private boost::noncopyable { + public: + TestAssertionFailureDetails( const std::string &theFile, + unsigned theLine, + const std::string &theMessage ); + + const std::string file; + const unsigned line; + const std::string message; + }; - class MyAssertionException : private boost::noncopyable { + /** + * Exception thrown when a test assertion fails. + * + * Typically thrown by helpers in the TestAssertion class and its ilk, below. + * + * NOTE(schwerin): This intentionally does _not_ extend std::exception, so that code under + * test that (foolishly?) catches std::exception won't swallow test failures. Doesn't + * protect you from code that foolishly catches ..., but you do what you can. + */ + class TestAssertionFailureException { public: - MyAssertionException() { - ss << "assertion: "; - } - std::stringstream ss; + TestAssertionFailureException( const std::string &theFile, + unsigned theLine, + const std::string &theMessage ); + + const std::string &getFile() const { return _details->file; } + unsigned getLine() const { return _details->line; } + const std::string &getMessage() const { return _details->message; } + + std::string toString() const; + + private: + boost::shared_ptr<TestAssertionFailureDetails> _details; }; - class MyAsserts : private boost::noncopyable { + /** + * Object representing an assertion about some condition. + */ + class TestAssertion : private boost::noncopyable { + public: - MyAsserts( const char * aexp , const char * bexp , const char * file , unsigned line ) - : _aexp( aexp ) , _bexp( bexp ) , _file( file ) , _line( line ) { + TestAssertion( const std::string &file, unsigned line ); + ~TestAssertion(); + void fail( const std::string &message) const; + void failIf( bool flag, const std::string &message ) const { + if ( flag ) fail( message ); + } + void failUnless( bool flag, const std::string &message ) const { + failIf( !flag, message ); } - template<typename A,typename B> - void ae( const A &a , const B &b ) { - _gotAssert(); - if ( a == b ) - return; + private: + const std::string _file; + const unsigned _line; + }; + + /** + * Specialization of TestAssertion for binary comparisons. + */ + class ComparisonAssertion : private TestAssertion { + public: + ComparisonAssertion( const std::string &aexp , const std::string &bexp , + const std::string &file , unsigned line ); - printLocation(); + template<typename A,typename B> + void assertEqual( const A &a , const B &b ) { + failUnless(a == b, getComparisonFailureMessage("==", a, b)); + } - MyAssertionException * e = getBase(); - e->ss << a << " != " << b << std::endl; - log() << e->ss.str() << std::endl; - throw e; + template<typename A,typename B> + void assertNotEqual( const A &a , const B &b ) { + failUnless(a != b, getComparisonFailureMessage("!=", a, b)); } template<typename A,typename B> - void nae( const A &a , const B &b ) { - _gotAssert(); - if ( a != b ) - return; + void assertLessThan( const A &a , const B &b ) { + failUnless(a < b, getComparisonFailureMessage("<", a, b)); + } - printLocation(); + template<typename A,typename B> + void assertNotLessThan( const A &a , const B &b ) { + failUnless(a >= b, getComparisonFailureMessage(">=", a, b)); + } - MyAssertionException * e = getBase(); - e->ss << a << " == " << b << std::endl; - log() << e->ss.str() << std::endl; - throw e; + template<typename A,typename B> + void assertGreaterThan( const A &a , const B &b ) { + failUnless(a > b, getComparisonFailureMessage(">", a, b)); } - void printLocation(); + template<typename A,typename B> + void assertNotGreaterThan( const A &a , const B &b ) { + failUnless(a <= b, getComparisonFailureMessage("<=", a, b)); + } private: - - void _gotAssert(); - - MyAssertionException * getBase(); + template< typename A, typename B> + std::string getComparisonFailureMessage(const std::string &theOperator, + const A &a, const B &b); std::string _aexp; std::string _bexp; - std::string _file; - unsigned _line; }; /** - * Returns the name of the currently executing unit test + * Hack to support the runaway test observer in dbtests. This is a hook that + * unit test running harnesses (unittest_main and dbtests) must implement. */ - std::string getExecutingTestName(); + void onCurrentTestNameChange( const std::string &testName ); /** * Return a list of suite names. @@ -212,3 +375,5 @@ namespace mongo { } // namespace unittest } // namespace mongo + +#include "mongo/unittest/unittest-inl.h" diff --git a/src/mongo/unittest/unittest_main.cpp b/src/mongo/unittest/unittest_main.cpp new file mode 100644 index 00000000000..7f685ae4362 --- /dev/null +++ b/src/mongo/unittest/unittest_main.cpp @@ -0,0 +1,12 @@ +// mongo/unittest/unittest_main.cpp + +#include <string> +#include <vector> + +#include "mongo/unittest/unittest.h" + +int main( int argc, char **argv ) { + return ::mongo::unittest::Suite::run(std::vector<std::string>(), ""); +} + +void mongo::unittest::onCurrentTestNameChange( const std::string &testName ) {} diff --git a/src/mongo/unittest/unittest_test.cpp b/src/mongo/unittest/unittest_test.cpp new file mode 100644 index 00000000000..f879158767b --- /dev/null +++ b/src/mongo/unittest/unittest_test.cpp @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2012 10gen Inc. + */ + +/** + * Unit tests of the unittest framework itself. + */ + +#include "mongo/unittest/unittest.h" + +#include <string> + +namespace { + +#define ASSERT_TEST_FAILS(TEST_EXPR) \ + ASSERT_THROWS((TEST_EXPR), mongo::unittest::TestAssertionFailureException) + + TEST(UnitTestSelfTest, DoNothing) { + } + + void throwSomething() { throw std::exception(); } + + TEST(UnitTestSelfTest, TestAssertThrowsSuccess) { + ASSERT_THROWS(throwSomething(), ::std::exception); + } + + TEST(UnitTestSelfTest, TestSuccessfulNumericComparisons) { + ASSERT_EQUALS(1LL, 1.0); + ASSERT_NOT_EQUALS(1LL, 0.5); + ASSERT_LESS_THAN(1, 5); + ASSERT_LESS_THAN_OR_EQUALS(1, 5); + ASSERT_LESS_THAN_OR_EQUALS(5, 5); + ASSERT_GREATER_THAN(5, 1); + ASSERT_GREATER_THAN_OR_EQUALS(5, 1); + ASSERT_GREATER_THAN_OR_EQUALS(5, 5); + } + + TEST(UnitTestSelfTest, TestNumericComparisonFailures) { + ASSERT_TEST_FAILS(ASSERT_EQUALS(10, 1LL)); + ASSERT_TEST_FAILS(ASSERT_NOT_EQUALS(10, 10LL)); + ASSERT_TEST_FAILS(ASSERT_LESS_THAN(10, 10LL)); + ASSERT_TEST_FAILS(ASSERT_GREATER_THAN(10, 10LL)); + ASSERT_TEST_FAILS(ASSERT_NOT_LESS_THAN(9, 10LL)); + ASSERT_TEST_FAILS(ASSERT_NOT_GREATER_THAN(1, 0LL)); + } + + TEST(UnitTestSelfTest, TestStringComparisons) { + ASSERT_EQUALS("hello", "hello"); + ASSERT_EQUALS(std::string("hello"), "hello"); + ASSERT_EQUALS("hello", std::string("hello")); + + ASSERT_NOT_EQUALS("hello", "good bye!"); + ASSERT_NOT_EQUALS(std::string("hello"), "good bye!"); + ASSERT_NOT_EQUALS("hello", std::string("good bye!")); + + ASSERT_TEST_FAILS(ASSERT_NOT_EQUALS("hello", "hello")); + ASSERT_TEST_FAILS(ASSERT_NOT_EQUALS(std::string("hello"), "hello")); + ASSERT_TEST_FAILS(ASSERT_NOT_EQUALS("hello", std::string("hello"))); + + ASSERT_TEST_FAILS(ASSERT_EQUALS("hello", "good bye!")); + ASSERT_TEST_FAILS(ASSERT_EQUALS(std::string("hello"), "good bye!")); + ASSERT_TEST_FAILS(ASSERT_EQUALS("hello", std::string("good bye!"))); + } + +} // namespace |