diff --git a/License.txt b/License.txt new file mode 100644 index 0000000..36b7cd9 --- /dev/null +++ b/License.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..8c6f756 --- /dev/null +++ b/README.txt @@ -0,0 +1,38 @@ +© 2009, Jon Stewart + +Scope is a simple, lightweight unit testing framework for C++. + +It should be easy to create tests, so it's important for Scope to scale down +when required. C++ is also a notoriously arcane language, with lots of gotchas. +Scope should avoid most of them, and give you a heads up about the ones you +could run into when using it. It should also adhere to as many of the C++ idioms +as possible, rather than copying idioms from other languages which aren't a good +fit in C++. + +In Scope, tests are free functions. Macros are used to auto-register them, and +there's no need to register sets of tests in some other source file. When setup +and teardown functionality is needed, macros allow the test writer to specify +the type of an object, which is then created immediately before the test is run, +passed to it as a parameter, and destroyed immediately after the test runs, i.e. +Scope uses constructors and destructors instead of separate setup() and +teardown() functions. + +Scope's autoregistration system does use a bunch of static variables, as well as +a static singleton to collect them, but it avoids many of the pitfalls inherit +with singletons and static allocation in C++. In particular, it does not depend +on the order of construction (or guarantees it using a Meyers singleton) and it +does not use heap-allocated memory (i.e. no calls to new or malloc() before +main()) in accordance with the standard. Among other things, this makes leak- +detectors much easier to use, since Scope shouldn't generate any noise. + +Tests are organized into sets, but sets can be relational, not just +hierarchical--Scope uses a graph to represent the connections between sets and +tests. One set per source file is automatically created, and each test is put +into its corresponding source set automatically, giving you a hierarchical +structure out of the box. However, it's possible to specify that a test should +belong to other sets. + +Scope needs more work with respect to command-line features, friendly output, +and performance profiling. + +Scope is released under the Boost license. See License.txt for details. diff --git a/SConstruct b/SConstruct new file mode 100644 index 0000000..0166613 --- /dev/null +++ b/SConstruct @@ -0,0 +1,17 @@ +# Copyright 2009, Jon Stewart +# Released under the terms of the Boost license (http://www.boost.org/LICENSE_1_0.txt). See License.txt for details. + +import os +import glob + +def getVar(name, defaultLoc): + try: + var = os.environ[name] + return var + except: + print(' * You need to set the %s environment variable' % name) + print(' e.g. export %s=%s' % (name, defaultLoc)) + sys.exit() + +env = Environment(ENV = os.environ, CPPPATH = [getVar('BOOST_HOME', '~/software/boost_1_38_0')], CCFLAGS = '-Wall -Wextra') +env.Command('dummy', env.Program('test', glob.glob('*.cpp')), './$SOURCE') diff --git a/main.cpp b/main.cpp new file mode 100755 index 0000000..85db2b9 --- /dev/null +++ b/main.cpp @@ -0,0 +1,12 @@ +/* + © 2009, Jon Stewart + Released under the terms of the Boost license (http://www.boost.org/LICENSE_1_0.txt). See License.txt for details. +*/ + +#include + +#include "scope/testrunner.h" + +int main(int argc, char *argv[]) { + return scope::DefaultRun(std::cout, argc, argv) ? 0: 1; +} diff --git a/scope/test.h b/scope/test.h new file mode 100755 index 0000000..2a508e8 --- /dev/null +++ b/scope/test.h @@ -0,0 +1,350 @@ +/* + © 2009, Jon Stewart + Released under the terms of the Boost license (http://www.boost.org/LICENSE_1_0.txt). See License.txt for details. +*/ + +#pragma once + +#include +#include +#include +#include + +namespace scope { + typedef std::list< std::string > MessageList; // need to replace this with an output iterator + typedef void (*TestFunction)(void); + + void RunFunction(TestFunction test, const char* testname, MessageList& messages); + void CaughtBadExceptionType(const std::string& testname, const std::string& msg); + + class test_failure : public std::runtime_error { + public: + test_failure(const char* const file, int line, const char *const message): + std::runtime_error(message), File(file), Line(line) + { + } + + virtual const char* what(void) { + std::stringstream ret; + ret << File << ':' << Line << ": " << std::runtime_error::what(); + return ret.str().c_str(); + } + + private: + const char *const File; + int Line; + }; + + template + void eval_condition(bool good, const char* const file, int line, const char *const expression) { + if (!good) { + throw ExceptionType(file, line, expression); + } + } + + template // it'd be good to have a CTAssert on (ExpectedT==ActualT) + void eval_equal(ExpectedT e, ActualT a, const char* const file, int line, const char* msg = "") { + if (!((e) == (a))) { + std::stringstream buf; + if (*msg) { + buf << msg << " "; + } + buf << "Expected: " << e << ", Actual: " << a; + throw ExceptionType(file, line, buf.str().c_str()); + } + } + + struct TestCommon { + TestCommon(const std::string& name): Name(name) {} + virtual ~TestCommon() {} + + std::string Name; + }; + + class Test : public TestCommon { + public: + Test(const std::string& name): TestCommon(name) {} + virtual ~Test() {} + + void Run(MessageList& messages) { + _Run(messages); + } + + private: + virtual void _Run(MessageList& messages) = 0; + }; + + class BoundTest : public Test { + public: + TestFunction Fn; + + BoundTest(const std::string& name, TestFunction fn): + Test(name), Fn(fn) {} + + private: + virtual void _Run(MessageList& messages) { + RunFunction(Fn, Name.c_str(), messages); + } + }; + + class SetTest : public Test { + public: + SetTest(const std::string& name): Test(name) {} + + private: + virtual void _Run(MessageList&) {} + }; + + template FixtureType* DefaultFixtureConstruct() { + return new FixtureType; + } + + template class FixtureTest: public Test { + public: + typedef void (*FixtureTestFunction)(FixtureT&); + typedef FixtureT* (*FixtureCtorFunction)(void); + + FixtureTestFunction Fn; + FixtureCtorFunction Ctor; + + FixtureTest(const std::string& name, FixtureTestFunction fn, FixtureCtorFunction ctor): Test(name), Fn(fn), Ctor(ctor) {} + + private: + virtual void _Run(MessageList& messages) { + try { + FixtureT *fixture((*Ctor)()); + try { + (*Fn)(*fixture); + } + catch(test_failure fail) { + messages.push_back(Name + ": " + fail.what()); + } + catch(std::exception except) { + messages.push_back(Name + ": " + except.what()); + } + catch(...) { + CaughtBadExceptionType(Name, "test threw unknown exception type"); + throw; + } + try { + delete fixture; + } + catch(test_failure fail) { + messages.push_back(Name + ": " + fail.what()); + } + catch(std::exception except) { + messages.push_back(Name + ": " + except.what()); + } + catch(...) { + CaughtBadExceptionType(Name, "teardown threw unknown exception type"); + throw; + } + } + catch(test_failure fail) { + messages.push_back(Name + ": " + fail.what()); + } + catch(std::exception except) { + messages.push_back(Name + ": " + except.what()); + } + catch(...) { + CaughtBadExceptionType(Name, "setup threw unknown exception type"); + throw; + } + } + }; + + class AutoRegister; + class CreateEdge; + + class TestRunner { + friend class CreateEdge; + public: + static TestRunner& Get(void); + + virtual void Run(MessageList& messages, const std::string& nameFilter = "") = 0; + +// virtual size_t Add(TestPtr test) = 0; + + virtual void addTest(AutoRegister& test) = 0; + virtual void addLink(CreateEdge& link) = 0; + virtual void CreateLink(const AutoRegister& from, const AutoRegister& to) = 0; + virtual unsigned int numTests() const = 0; + virtual void setDebug(bool) = 0; + + protected: + TestRunner() {} + virtual ~TestRunner() {} + + virtual void FastCreateLink(const AutoRegister& from, const AutoRegister& to) = 0; + + private: + TestRunner(const TestRunner&); + }; + + template class Node { + public: + void insert(T& node) { + node.Next = Next; + Next = &node; + } + + T* Next; + }; + + class AutoRegister: public Node { + public: + size_t Index; + const char* TestName; + + AutoRegister(const char* name): + Index(0), TestName(name) + { + TestRunner::Get().addTest(*this); + } + + virtual ~AutoRegister() {} + + virtual Test* Construct() = 0; + }; + + class AutoRegisterSet: public AutoRegister { + public: + + AutoRegisterSet(const char* name): AutoRegister(name) {} + virtual ~AutoRegisterSet() {} + + virtual Test* Construct() { + return new SetTest(TestName); + } + }; + + class AutoRegisterSimple: public AutoRegister { + public: + TestFunction Fn; + + AutoRegisterSimple(const char* name, TestFunction fn): AutoRegister(name), Fn(fn) {} + virtual ~AutoRegisterSimple() {} + + virtual Test* Construct() { + return new BoundTest(TestName, Fn); + } + }; + + template class AutoRegisterFixture: public AutoRegister { + public: + typedef void (*FixtureTestFunction)(FixtureT&); + typedef FixtureT* (*FixtureCtorFunction)(void); + + FixtureTestFunction Fn; + FixtureCtorFunction Ctor; + + AutoRegisterFixture(const char* name, FixtureTestFunction fn, FixtureCtorFunction ctor): AutoRegister(name), Fn(fn), Ctor(ctor) {} + virtual ~AutoRegisterFixture() {} + + virtual Test* Construct() { + return new FixtureTest(TestName, Fn, Ctor); + } + }; + + class CreateEdge: public Node { + public: + CreateEdge(const AutoRegister& from, const AutoRegister& to): + From(from), To(to) + { + TestRunner::Get().addLink(*this); + } + + const AutoRegister &From, + &To; + }; + + namespace { + AutoRegister& GetTranslationUnitSet() { + static AutoRegisterSet singleton(__FILE__); + return singleton; + } + } + + namespace user_defined {} + +} + +#define SCOPE_CAT(s1, s2) s1##s2 +#define SCOPE_UNIQUENAME(base) SCOPE_CAT(base, __LINE__) + +// puts the auto-register variable into a unique -- but reference-able -- namespace. +// the auto-register variable can then contain some useful information which we can reference inside of the test. +// I wish that the testname could be the name of the innermost namespace, but C++ seems to be confused by the re-use of the identifier. +// thus, "ns" is trivially appended to ${testname} so as not to confuse C++. +// the requirements put on the namespace are isomorphic to the reqs we use for the scope of the test function, i.e. +// as long as "void testname(void) {}" works in the global scope, the namespacing will work, too. +// if "void testname(void) {}" results in a multiple-symbol linker error, then so will the namespacing. + + +#define SCOPE_TEST_AUTO_REGISTRATION(testname) \ + namespace scope { namespace user_defined { namespace { namespace SCOPE_CAT(testname, ns) { \ + AutoRegisterSimple reg(#testname, testname); \ + CreateEdge SCOPE_CAT(testname, translation_unit)(GetTranslationUnitSet(), reg); \ + } } } } + +#define SCOPE_TEST(testname) \ + void testname(void); \ + SCOPE_TEST_AUTO_REGISTRATION(testname); \ + void testname(void) + +#define SCOPE_SET_WITH_NAME(setidentifier, setname) \ + namespace scope { namespace user_defined { namespace did_you_forget_to_define_your_set { \ + AutoRegisterSet setidentifier(setname); \ + CreateEdge SCOPE_CAT(setidentifier, translation_unit)(GetTranslationUnitSet(), setidentifier); \ + } } } + +#define SCOPE_SET(setname) \ + SCOPE_SET_WITH_NAME(setname, #setname) + +#define SCOPE_TEST_BELONGS_TO(testname, setname) \ + namespace scope { namespace user_defined { namespace did_you_forget_to_define_your_set { \ + extern AutoRegisterSet setname; } \ + namespace { namespace SCOPE_CAT(testname, ns) { \ + using namespace scope::user_defined::did_you_forget_to_define_your_set; \ + CreateEdge SCOPE_CAT(setname, edgecreator)(setname, reg); \ + } } } } + +#define SCOPE_FIXTURE_AUTO_REGISTRATION(fixtureType, testfunction, ctorfunction) \ + namespace scope { namespace user_defined { namespace { namespace SCOPE_CAT(testfunction, ns) { \ + AutoRegisterFixture reg(#testfunction, testfunction, ctorfunction); \ + CreateEdge SCOPE_CAT(testfunction, translation_unit)(GetTranslationUnitSet(), reg); \ + } } } } + +#define SCOPE_FIXTURE(testname, fixtureType) \ + void testname(fixtureType& fixture); \ + SCOPE_FIXTURE_AUTO_REGISTRATION(fixtureType, testname, &DefaultFixtureConstruct); \ + void testname(fixtureType& fixture) + +#define SCOPE_FIXTURE_CTOR(testname, fixtureType, ctorExpr) \ + void testname(fixtureType& fixture); \ + namespace scope { namespace user_defined { namespace { namespace SCOPE_CAT(testname, ns) { \ + fixtureType* fixConstruct() { return new ctorExpr; } \ + } } } } \ + SCOPE_FIXTURE_AUTO_REGISTRATION(fixtureType, testname, scope::user_defined::SCOPE_CAT(testname, ns)::fixConstruct); \ + void testname(fixtureType& fixture) + +#define SCOPE_ASSERT_THROW(condition, exceptiontype) \ + scope::eval_condition< exceptiontype >((condition) ? true: false, __FILE__, __LINE__, #condition) + +#define SCOPE_ASSERT(condition) \ + SCOPE_ASSERT_THROW(condition, scope::test_failure) + +#define SCOPE_ASSERT_EQUAL(expected, actual) \ + scope::eval_equal< scope::test_failure >((expected), (actual), __FILE__, __LINE__) + +#define SCOPE_ASSERT_EQUAL_MSG(expected, actual, msg) \ + scope::eval_equal< scope::test_failure >((expected), (actual), __FILE__, __LINE__, msg) + +#define SCOPE_EXPECT(statement, exception) \ + try { \ + statement; \ + throw scope::test_failure(__FILE__, __LINE__, "Expected exception not caught"); \ + } \ + catch(exception) { \ + ; \ + } diff --git a/scope/testrunner.h b/scope/testrunner.h new file mode 100755 index 0000000..0def988 --- /dev/null +++ b/scope/testrunner.h @@ -0,0 +1,174 @@ +/* + © 2009, Jon Stewart + Released under the terms of the Boost license (http://www.boost.org/LICENSE_1_0.txt). See License.txt for details. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "test.h" + +namespace scope { + + void RunFunction(scope::TestFunction test, const char* testname, MessageList& messages) { + try { + test(); + } + catch(test_failure fail) { + messages.push_back(std::string(testname) + ": " + fail.what()); + } + catch(std::exception except) { + messages.push_back(std::string(testname) + ": " + except.what()); + } + catch(...) { + CaughtBadExceptionType(testname, "test threw unrecognized type"); + throw; + } + } + + void CaughtBadExceptionType(const std::string& name, const std::string& msg) { + std::cerr << name << ": " << msg << "; please at least inherit from std::exception" << std::endl; + } + + namespace { + class TestRunnerImpl : public TestRunner { + + typedef boost::adjacency_list< boost::vecS, boost::vecS, boost::directedS, boost::property< boost::vertex_color_t, boost::default_color_type > > TestGraph; + typedef boost::graph_traits::vertex_descriptor Vertex; + typedef boost::graph_traits::vertex_iterator VertexIter; + typedef boost::property_map::type Color; + typedef boost::property_map::type IndexMap; + typedef boost::shared_ptr< Test > TestPtr; + typedef std::vector< TestPtr > TestMap; + + class TestVisitor : public boost::default_dfs_visitor { + private: + TestMap& Tests; + MessageList& Messages; + std::string NameFilter; + bool Debug; + + public: + TestVisitor(TestMap& tests, MessageList& messages, const std::string& nameFilt, bool debug): Tests(tests), Messages(messages), NameFilter(nameFilt), Debug(debug) {} + TestVisitor(const TestVisitor& x): Tests(x.Tests), Messages(x.Messages), NameFilter(x.NameFilter), Debug(x.Debug) {} + + template void discover_vertex(Vertex v, const Graph& g) const { + using namespace boost; + //std::cerr << "Running " << Tests[get(vertex_index, g)[v]]->Name << "\n"; + std::string name(Tests[get(vertex_index, g)[v]]->Name); + if (NameFilter.empty() || NameFilter == name) { + if (Debug) { + std::cerr << "Running " << name << std::endl; + } + Tests[get(vertex_index, g)[v]]->Run(Messages); + if (Debug) { + std::cerr << "Done with " << name << std::endl; + } + } + } + }; + + public: + TestRunnerImpl(): + FirstTest(0), FirstEdge(0), NumTests(0), Debug(false) {} + + virtual void Run(MessageList& messages, const std::string& nameFilter) { + using namespace boost; + for (AutoRegister* cur = FirstTest; cur != 0; cur = cur->Next) { + Add(TestPtr(cur->Construct())); + } + for (CreateEdge* cur = FirstEdge; cur; cur = cur->Next) { + CreateLink(cur->From, cur->To); + } + TestVisitor vis(Tests, messages, nameFilter, Debug); + depth_first_search(Graph, vis, get(vertex_color, Graph)); + /* for(std::pair< VertexIter, VertexIter > vipair(vertices(Graph)); vipair.first != vipair.second; ++vipair.first) { + Tests[get(vertex_index, Graph)[*vipair.first]]->Run(messages); + }*/ + } + + virtual size_t Add(TestPtr test) { + using namespace boost; + Vertex v(add_vertex(Graph)); + size_t ret = get(vertex_index, Graph)[v]; + assert(ret == Tests.size()); + Tests.push_back(test); + if (!dynamic_pointer_cast(test)) { + ++NumTests; + } + return ret; + } + + virtual void CreateLink(const AutoRegister& from, const AutoRegister& to) { + FastCreateLink(from, to); // we should check for cycles. maybe? + } + + virtual void FastCreateLink(const AutoRegister& from, const AutoRegister& to) { + boost::add_edge(from.Index, to.Index, Graph); + } + + virtual void addTest(AutoRegister& test) { + if (0 == FirstTest) { + FirstTest = &test; + } + else { + FirstTest->insert(test); + } + } + + virtual void addLink(CreateEdge&) { + + } + + virtual unsigned int numTests() const { + return NumTests; + } + + virtual void setDebug(bool val) { + Debug = val; + } + + private: + TestGraph Graph; + TestMap Tests; + AutoRegister* FirstTest; + CreateEdge* FirstEdge; + unsigned int NumTests; + bool Debug; + }; + } + + TestRunner& TestRunner::Get(void) { + static TestRunnerImpl singleton; + return singleton; + } + + bool DefaultRun(std::ostream& out, int argc, char** argv) { + MessageList msgs; + TestRunner &runner(TestRunner::Get()); + std::string debug("--debug"); + if ((argc == 3 && debug == argv[2]) || (argc == 4 && debug == argv[3])) { + runner.setDebug(true); + } + std::string nameFilter(argc > 2 && debug != argv[2] ? argv[2]: ""); + runner.Run(msgs, nameFilter); + + for(MessageList::const_iterator it(msgs.begin()); it != msgs.end(); ++it) { + out << *it << '\n'; + } + if (msgs.begin() == msgs.end()) { + out << "OK (" << runner.numTests() << " tests)" << std::endl; + return true; + } + else { + out << "Failures!" << std::endl; + out << "Tests run: " << runner.numTests() << ", Failures: " << msgs.size() << std::endl; + return false; + } + } +} diff --git a/test1.cpp b/test1.cpp new file mode 100755 index 0000000..ea28e0d --- /dev/null +++ b/test1.cpp @@ -0,0 +1,25 @@ +/* + © 2009, Jon Stewart + Released under the terms of the Boost license (http://www.boost.org/LICENSE_1_0.txt). See License.txt for details. +*/ + +#include "scope/test.h" + +SCOPE_TEST(simpleTest) { + SCOPE_ASSERT(true); +} + +SCOPE_TEST_BELONGS_TO(simpleTest, coolio); + +SCOPE_TEST(failTest) { + SCOPE_ASSERT(false); // ha-ha! +} + +void doNothing() { + ; +} + +SCOPE_TEST(TestExpectMacro) { + SCOPE_EXPECT(throw int(1), int); + SCOPE_EXPECT(doNothing(), int); +} diff --git a/test2.cpp b/test2.cpp new file mode 100755 index 0000000..66c0c20 --- /dev/null +++ b/test2.cpp @@ -0,0 +1,53 @@ +/* + © 2009, Jon Stewart + Released under the terms of the Boost license (http://www.boost.org/LICENSE_1_0.txt). See License.txt for details. +*/ + +#include "scope/test.h" + +struct Fixture1 { + Fixture1(): + String("cool"), Int(42) {} + virtual ~Fixture1() {} + + std::string String; + int Int; +}; + +SCOPE_FIXTURE(fix1, Fixture1) { + SCOPE_ASSERT(std::string("cool") == fixture.String); + SCOPE_ASSERT_EQUAL(42, fixture.Int); + SCOPE_ASSERT_EQUAL_MSG(41, fixture.Int, "silly"); +} + +struct Fixture2: public Fixture1 { + Fixture2() { + SCOPE_ASSERT(!"Fixture2's constructor threw"); + } +}; + +SCOPE_FIXTURE(badSetup, Fixture2) { + SCOPE_ASSERT_EQUAL(41, fixture.Int); // should not be called +} + +struct Fixture3: public Fixture1 { + ~Fixture3() { + SCOPE_ASSERT(!"Fixture3's destructor threw"); + } +}; + +SCOPE_FIXTURE(badTeardown, Fixture3) { + SCOPE_ASSERT_EQUAL(42, fixture.Int); +} + +struct Fixture4: public Fixture1 { + Fixture4(int i): + Fixture1() + { + Int = i; + } +}; + +SCOPE_FIXTURE_CTOR(customFixture, Fixture1, Fixture4(7)) { + SCOPE_ASSERT_EQUAL(7, fixture.Int); +} diff --git a/testdefines.cpp b/testdefines.cpp new file mode 100755 index 0000000..e2e36d1 --- /dev/null +++ b/testdefines.cpp @@ -0,0 +1,8 @@ +/* + © 2009, Jon Stewart + Released under the terms of the Boost license (http://www.boost.org/LICENSE_1_0.txt). See License.txt for details. +*/ + +#include "scope/test.h" + +SCOPE_SET(coolio);