From cdbe51a3ccfe265161ba9c30878477ff433d5df1 Mon Sep 17 00:00:00 2001 From: Tom Hicks Date: Tue, 11 Apr 2023 23:42:27 -0700 Subject: [PATCH] Finally have something useful. --- .gitignore | 1 + Makefile | 33 ++- README.md | 33 +++ examples/JTest.cpp | 251 ----------------- examples/JTest.h | 85 ------ examples/example.cpp | 162 +++-------- examples/example.o | Bin 9672 -> 0 bytes include/JTest/ConfigureFunction.h | 10 + include/JTest/DescribeOptions.h | 29 ++ include/JTest/Expectable.h | 259 ++++++++++++++++++ include/JTest/FailedExpectation.h | 16 ++ include/JTest/JTest.h | 39 +++ include/JTest/Test.h | 17 ++ include/JTest/TestBundle.h | 33 +++ include/JTest/TestException.h | 28 ++ include/JTest/TestFailedException.h | 20 ++ include/JTest/TestFunction.h | 7 + include/JTest/TestOptions.h | 12 + include/JTest/TestPendingException.h | 17 ++ include/JTest/TestResults.h | 28 ++ include/JTest/TestStatus.h | 11 + src/JTest/ConfigureFunction.cpp | 20 ++ src/JTest/DescribeOptions.cpp | 44 +++ src/JTest/FailedExpectation.cpp | 11 + src/JTest/JTest.cpp | 201 ++++++++++++++ JTest.cpp => src/JTest/Test.cpp | 0 src/JTest/TestBundle.cpp | 52 ++++ src/JTest/TestException.cpp | 47 ++++ src/JTest/TestFailedException.cpp | 22 ++ include/JTest.h => src/JTest/TestFunction.cpp | 0 src/JTest/TestOptions.cpp | 0 src/JTest/TestPendingException.cpp | 17 ++ src/JTest/TestResults.cpp | 35 +++ src/JTest/TestStatus.cpp | 0 34 files changed, 1065 insertions(+), 475 deletions(-) delete mode 100644 examples/JTest.cpp delete mode 100644 examples/JTest.h delete mode 100644 examples/example.o create mode 100644 include/JTest/ConfigureFunction.h create mode 100644 include/JTest/DescribeOptions.h create mode 100644 include/JTest/Expectable.h create mode 100644 include/JTest/FailedExpectation.h create mode 100644 include/JTest/JTest.h create mode 100644 include/JTest/Test.h create mode 100644 include/JTest/TestBundle.h create mode 100644 include/JTest/TestException.h create mode 100644 include/JTest/TestFailedException.h create mode 100644 include/JTest/TestFunction.h create mode 100644 include/JTest/TestOptions.h create mode 100644 include/JTest/TestPendingException.h create mode 100644 include/JTest/TestResults.h create mode 100644 include/JTest/TestStatus.h create mode 100644 src/JTest/ConfigureFunction.cpp create mode 100644 src/JTest/DescribeOptions.cpp create mode 100644 src/JTest/FailedExpectation.cpp create mode 100644 src/JTest/JTest.cpp rename JTest.cpp => src/JTest/Test.cpp (100%) create mode 100644 src/JTest/TestBundle.cpp create mode 100644 src/JTest/TestException.cpp create mode 100644 src/JTest/TestFailedException.cpp rename include/JTest.h => src/JTest/TestFunction.cpp (100%) create mode 100644 src/JTest/TestOptions.cpp create mode 100644 src/JTest/TestPendingException.cpp create mode 100644 src/JTest/TestResults.cpp create mode 100644 src/JTest/TestStatus.cpp diff --git a/.gitignore b/.gitignore index a0ead52..ac28d01 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .vscode build tmp +*.o diff --git a/Makefile b/Makefile index aac9e7f..2132196 100644 --- a/Makefile +++ b/Makefile @@ -1,32 +1,39 @@ BUILD_DIR ?= build ARCHS = -arch arm64 # -arch x86_64 +INCLUDES = -I./include CC = clang++ -CCFLAGS += -std=c++17 -finput-charset=utf-8 -fdiagnostics-show-template-tree -fno-elide-type -g $(ARCHS) --stdlib=libc++ +CCFLAGS += -std=c++17 -finput-charset=utf-8 -fdiagnostics-show-template-tree -fno-elide-type -g $(ARCHS) $(INCLUDES) LD = clang++ LDFLAGS += $(ARCHS) +JTEST_HEADERS := $(wildcard include/JTest/*.h) +JTEST_SOURCES := $(wildcard src/JTest/*.cpp) +JTEST_OBJECTS := $(patsubst src/%.cpp, $(BUILD_DIR)/%.o, $(JTEST_SOURCES)) +#$(patsubst src/%.cpp, $(BUILD_DIR)/%.o, $(JTEST_SOURCES)) -.Phony: clean all $(BUILD_DIR)/test.txt +.Phony: clean all run -all: $(BUILD_DIR)/test.txt $(BUILD_DIR)/examples/example - @echo $(BUILD_DIR) +all: $(BUILD_DIR)/examples/example clean: rm -rf $(BUILD_DIR) mkdir -p $(BUILD_DIR)/examples - @echo CC=$(CC) - @echo CCFLAGS=$(CCFLAGS) - @echo LD=$(LD) - @echo LDFLAGS=$(LDFLAGS) + mkdir -p $(BUILD_DIR)/JTest +# @echo JTEST_HEADERS = $(JTEST_HEADERS) +# @echo JTEST_SOURCES = $(JTEST_SOURCES) +# @echo JTEST_OBJECTS = $(JTEST_OBJECTS) -$(BUILD_DIR)/test.txt: - @echo $(BUILD_DIR)/test.txt +run: $(BUILD_DIR)/examples/example + $(BUILD_DIR)/examples/example -$(BUILD_DIR)/examples/example: $(BUILD_DIR)/examples/example.o $(BUILD_DIR)/examples/JTest.o +$(BUILD_DIR)/examples/example: $(BUILD_DIR)/examples/example.o $(JTEST_OBJECTS) $(LD) $(LDFLAGS) -o $@ $^ -$(BUILD_DIR)/examples/example.o: examples/example.cpp examples/ClassToTest.h examples/JTest.h +$(BUILD_DIR)/examples/%.o: examples/%.cpp examples/ClassToTest.h $(JTEST_HEADERS) $(CC) $(CCFLAGS) -c -o $@ $< -$(BUILD_DIR)/examples/JTest.o: examples/JTest.cpp examples/JTest.h +$(BUILD_DIR)/examples/JTest.o: examples/JTest.cpp examples/JTest.h $(JTEST_HEADERS) + $(CC) $(CCFLAGS) -c -o $@ $< + +$(BUILD_DIR)/JTest/%.o: src/JTest/%.cpp $(JTEST_HEADERS) $(CC) $(CCFLAGS) -c -o $@ $< diff --git a/README.md b/README.md index e69de29..8a2751f 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,33 @@ +## TODO: +* Make Expectable toThrow methods work with arguments, class methods, static class methods, and possibly constructors/destructors. +* Report all errors at the end instead of printing them inline. This is needed for parallel support. +* Figure out how to make JTest self testing. Maybe use some namespace magic to run a special instance in another namespace. +* Mocks + * Design them. + * Add them. +* Spies + * Design them. + * Add them. +* Expectable + * Clean up failure messages in Expectable. + * Make failure messages in Expectable only use ostreams if operator<< is defined for T. + * Pick a name for Expectable::not() +* DescribeOptions + * Add operator<< and print for DescribeOptions +* JTest + * Make it and xit return a Test instead of a TestBundle + * Make describe and xdescribe take a variant of Test or TestBundle instead of just TestBundle + * Make TestOptions useful + * Find a way to set label/path from it/xit/describe/xdescribe instead of when we execute them. + * If executing a disabled bundle report all of it's child tests as skipped/disabled. + * Execute tests in parallel. + * Recheck that bundles/tests inherit before/after methods from their parents. + * Before* of parents should be called before that of children. + * After* of parents should be called after that of children. + * When reporting Unhandled exceptions in tests, report the fully qualified test name and label. +* TestResults + * Add commented out vectors to hold skipped, failed, errored test info. +* FailedExpectation + * Figure out a way to get label and path when creating the expectation from an Expect method. + * We need to get the label and path from the parent method (execute) that is calling expect(T) to create the Expectable. + * We will likely be creating the FailedExpectation exception where the test fails and adding the label and path when we catch it before reporting the failure. diff --git a/examples/JTest.cpp b/examples/JTest.cpp deleted file mode 100644 index 08c178c..0000000 --- a/examples/JTest.cpp +++ /dev/null @@ -1,251 +0,0 @@ -#include "JTest.h" -#include - -namespace JTest { - using std::ostream; - using std::endl; - using std::vector; - using std::string; - using std::runtime_error; - using std::string; - using std::optional; - - runtime_error unimplemented_function_error(string method_name) { - return runtime_error("Unimplemented function: " + method_name); - } - - runtime_error unimplemented_method_error(string class_name, string method_name) { - return runtime_error("Unimplemented method in class " + class_name + ": " + method_name); - } - - runtime_error unimplemented_feature_error(string feature_name) { - return runtime_error("Unimplemented feature: " + feature_name); - } - - testresults_t make_testresults() { - return {0, 0, 0, 0}; - } - - testresults_t make_testresults(uint32_t total, uint32_t skipped, uint32_t passed, uint32_t failed) { - return {total, skipped, passed, failed}; - } - - testresults_t add(const testresults_t& l, const testresults_t& r) { - return {l.total + r.total, l.skipped + r.skipped, l.passed + r.passed, l.failed + r.failed}; - } - - void print_test_results(const testresults_t& results, ostream& out) { - out << "Tests: " << results.total << endl; - out << "Failed: " << results.failed << ", Passed: " << results.passed << ", Skipped: " << results.skipped << endl; - } - - testresults_t operator+(const testresults_t& left, const testresults_t& right) { - return add(left, right); - } - - testresults_t& operator+=(testresults_t& left, const testresults_t& right) { - left = left + right; - return left; - } - - testbundle_t make_testbundle(const vector& bundles, const describeoptions_t& options) { - testbundle_t b; - - for_each(bundles.begin(), bundles.end(), [&b](testbundle_t bundle){ - if (bundle._tests.size() == 0 && bundle._children.size() == 1 && bundle._label.size() == 0) { - b._tests.push_back(bundle._tests.at(0)); - } else { - b._children.push_back(bundle); - } - }); - - b._beforeEach = options.getBeforeEach(); - b._afterEach = options.getAfterEach(); - b._beforeAll = options.getBeforeAll(); - b._afterAll = options.getAfterAll(); - return b; - } - - describeoptions_t& describeoptions_t::beforeEach(configure_fn fn) { - this->_beforeEach = fn; - return *this; - } - - describeoptions_t& describeoptions_t::afterEach(configure_fn fn) { - this->_afterEach = fn; - return *this; - } - describeoptions_t& describeoptions_t::beforeAll(configure_fn fn) { - this->_beforeAll = fn; - return *this; - } - describeoptions_t& describeoptions_t::afterAll(configure_fn fn) { - this->_afterAll = fn; - return *this; - } - describeoptions_t make_describeoptions() { - return {}; - } - - // TODO test_method is the actual test method. We should save it to the test instead of executing it. - testbundle_t it(const string& label, const test_fn& test_method, optional options) { - // TODO: Stop ignoring options. - test_t test; - test._disabled = false; - test._label = label; - test._test_method = test_method; - return make_testbundle("", {test}); - } - - testbundle_t xit(const string& label, const test_fn& test_method, optional options) { - // TODO: replace this with a call to it and setting _disabled to true after it and xit are made to return test_t. - test_t test; - test._disabled = true; - test._label = label; - test._test_method = test_method; - return make_testbundle("", {test}); - } - - testresults_t execute(testbundle_t bundle) { - testresults_t results; - - if (bundle._disabled) { - // TODO: recursively report all tests as skipped. - } else { - try { - // TODO: Try to do this in parallel where possible - if (bundle._beforeAll.has_value()) { - bundle._beforeAll.value()(); - } - for_each(bundle._children.begin(), bundle._children.end(), [&results](testbundle_t bundle) { - // TODO: Find a way to make child tests get our beforeEach and afterEach callbacks. - results += execute(bundle); - }); - // TODO: Consider capturing these callbacks differently. Without the bundle? By value? - for_each(bundle._tests.begin(), bundle._tests.end(), [&results, &bundle](test_t test) { - if (bundle._beforeEach.has_value()) { - bundle._beforeEach.value()(); - } - results += execute(test); - if (bundle._afterEach.has_value()) { - bundle._afterEach.value()(); - } - }); - if (bundle._afterAll.has_value()) { - bundle._afterAll.value()(); - } - } catch(...) { - // TODO: Log this and mark the tests as failed. - // Report as much info as possible. This likely means something happened in beforeAll() or afterAll(). - } - } - throw unimplemented_function_error("execute(testbundle_t)"); - } - - testresults_t execute(test_t test) { - int status = 0; - if (test._disabled) { - - } - -/* - status = PASSED (PASSED, FAILED, SKIPPED (PENDING | DISABLED), QUEUED, RUNNING) QUEUED and RUNNING might not make sense. - If the test is marked as disabled (xit) then record it as a skipped test and return. - try { - test.beforeEach(); - run the test function - // Successful completion of the test method is recorded as a passed test. - // Pending tests throw a test_pending exception - These are recorded as skipped tests. - // Failed expects throw a test_failed exception - These are recorded as failed tests. - // Runtime errors and catchall should be caught last of all. - These are recorded as failed tests. - test.afterEach(); - - } catch (test_pending_exception ex) { - status = SKIPPED; - } catch (test_failed_exception ex) { - status = FAILED - } catch (exception ex) { - status = FAILED - } catch (...) { - status = FAILED - } finally { - maybe some kind of cleanup - } - - return make_testresults(...) // based on the status. -*/ - throw unimplemented_function_error("execute(test_t)"); - } - - optional combine(optional first, optional second) { - if (first.has_value()) { - auto first_v = first.value(); - if (second.has_value()) { - auto second_v = second.value(); - return [first_v, second_v]() { - first_v(); - second_v(); - }; - } else { - return first; - } - } else { - return second; - } - } - - testbundle_t describe(const string& label, const make_testbundle_fn& make_tests, optional options) { - testbundle_t bundle = make_tests(); - bundle._label = label; - bundle._disabled = false; - if (options.has_value()) { - describeoptions_t options_v = options.value(); - bundle._afterAll = combine(options_v.getAfterAll(), bundle._afterAll); - bundle._afterEach = combine(options_v.getAfterEach(), bundle._afterEach); - bundle._beforeAll = combine(options_v.getBeforeAll(), bundle._beforeAll); - bundle._beforeEach = combine(options_v.getBeforeEach(), bundle._beforeEach); - } - return bundle; - } - - testbundle_t make_testbundle(const string& label, const vector& tests) { - testbundle_t bundle; - bundle._label = label; - bundle._tests.clear(); - for_each(tests.begin(), tests.end(), [&bundle](test_t test){ - bundle._tests.push_back(test); - }); - return bundle; - } - - optional describeoptions_t::getAfterAll() const { - return this->_afterAll; - } - - optional describeoptions_t::getAfterEach() const { - return this->_afterEach; - } - - optional describeoptions_t::getBeforeAll() const { - return this->_beforeAll; - } - - optional describeoptions_t::getBeforeEach() const { - return this->_beforeEach; - } - - // TODO: Use these to make the unimplemented_* errors simpler to call. - // For this function - // testbundle_t describe(const std::string& label, const make_testbundle_fn& make_tests, std::optional options) - // __PRETTY_FUNCTION__ - // Unimplemented function: JTest::testbundle_t JTest::describe(const std::string &, const JTest::make_testbundle_fn &, std::optional) - // __FUNCSIG__ is not defined on clang++ - // __func__ - // describe - // __LINE__ is an integer - // - // __FILE__ - // examples/JSTest.cpp - // __FUNCTION__ - // describe -} diff --git a/examples/JTest.h b/examples/JTest.h deleted file mode 100644 index 6fe5630..0000000 --- a/examples/JTest.h +++ /dev/null @@ -1,85 +0,0 @@ -#include // TODO: Maybe just ostream. - -namespace JTest { - using std::ostream; - // TODO: Consider making testresults_t a class so we can hide the vectors behind accessor methods void add(...), T get(), vector get(uint32_t index) - - struct testbundle_t; - struct test_t; - typedef std::function test_fn; - typedef std::function make_testbundle_fn; - typedef std::function configure_fn; - - struct testresults_t { - uint32_t total; - uint32_t skipped; - uint32_t passed; - uint32_t failed; - // vector errors; - // vector failures; - // vector skipped; - }; - - struct test_t { - std::string _label; - test_fn _test_method; - bool _disabled; - }; - - struct testbundle_t { - std::string _label; - std::vector _tests; - std::vector _children; - std::optional _beforeEach; - std::optional _afterEach; - std::optional _beforeAll; - std::optional _afterAll; - bool _disabled; - }; - - struct describeoptions_t { - // TODO: Should these configure_fn params be const and/or &? - describeoptions_t& beforeEach(configure_fn); - describeoptions_t& afterEach(configure_fn); - describeoptions_t& beforeAll(configure_fn); - describeoptions_t& afterAll(configure_fn); - std::optional getBeforeEach() const; - std::optional getAfterEach() const; - std::optional getBeforeAll() const; - std::optional getAfterAll() const; - private: - std::optional _beforeEach; - std::optional _afterEach; - std::optional _beforeAll; - std::optional _afterAll; - }; - struct testoptions_t {}; - - testresults_t operator+(const testresults_t& left, const testresults_t& right); - - testresults_t& operator+=(testresults_t& left, const testresults_t& right); - - testresults_t make_testresults(); - testresults_t make_testresults(uint32_t total, uint32_t skipped, uint32_t passed, uint32_t failed); - testresults_t add(const testresults_t&, const testresults_t&); - void print_test_results(const testresults_t&, ostream&); - - // Executes the tests in tests. Possibly in parallel. Will block until all async tests have completed. - testresults_t execute(testbundle_t tests); - testresults_t execute(test_t test); - - // - testbundle_t describe(const std::string& label, const make_testbundle_fn& make_tests, std::optional options = std::nullopt); - testbundle_t xdescribe(const std::string& label, const make_testbundle_fn& make_tests, std::optional options = std::nullopt); - - testbundle_t make_testbundle(const std::vector& tests, const describeoptions_t& options); - - // TODO: Make this return a test_t instead. - testbundle_t it(const std::string& label, const test_fn& test_method, std::optional options = std::nullopt); - testbundle_t xit(const std::string& label, const test_fn& test_method, std::optional options = std::nullopt); - - describeoptions_t make_describeoptions(); - - testbundle_t make_testbundle(const std::string& label, const std::vector& tests); - -} diff --git a/examples/example.cpp b/examples/example.cpp index 0997613..91c8fee 100644 --- a/examples/example.cpp +++ b/examples/example.cpp @@ -1,12 +1,10 @@ -// #include -#include "JTest.h" -#include "ClassToTest.h" +#include #include #include #include #include #include -#include +#include "ClassToTest.h" using namespace JTest; using namespace MyNS; @@ -16,27 +14,31 @@ using std::vector; using std::cout; using std::exception; using std::endl; +using std::runtime_error; // const vector& might stop being a reference -testresults_t test_ClassToTest_main(const vector& argv) { +TestResults test_ClassToTest_main(const vector& argv) { return execute( describe("ClassToTest", [](){ - return make_testbundle( + return TestBundle( { it("should do the thing", [](){ // Throw exception if somethings goes wrong }), it("should do the other thing", [](){ + pending("we havent made the other thing yet"); }), it("should not do the bad thing", [](){ + fail("it did the bad thing"); }), it("should throw an exception if we do the other bad thing", [](){ + throw runtime_error("Bad thing happen."); }), }, - make_describeoptions() + DescribeOptions() .beforeEach([](){}) .afterEach([](){}) .beforeAll([](){}) @@ -45,46 +47,46 @@ testresults_t test_ClassToTest_main(const vector& argv) { ); } -testresults_t test_temp(const vector& argv) { +TestResults test_temp(const vector& args) { return execute( describe("ClassToTest", [](){ - return make_testbundle({ + return TestBundle({ describe("FeatureToTest", [](){ - return make_testbundle({ + return TestBundle({ // it("should do the thing", [](){ // }), // it("should not do the other thing", [](){ // }), - }, make_describeoptions()); + }, DescribeOptions()); }), - }, make_describeoptions()); + }, DescribeOptions()); }) ); } // Exmple of nested describes. -testresults_t test_something(const vector& argv) { +TestResults test_something(const vector& argv) { return execute( describe("", [](){ - return make_testbundle({ + return TestBundle({ describe("", [](){ - return make_testbundle({ + return TestBundle({ - }, make_describeoptions()); - }, make_describeoptions()), - }, make_describeoptions()); + }, DescribeOptions()); + }, DescribeOptions()), + }, DescribeOptions()); }) ); } -testresults_t test_ClassToTest_2(const vector& argv) { +TestResults test_ClassToTest_2(const vector& argv) { return execute( describe("ClassToTest", [](){ - return make_testbundle({ + return TestBundle({ }, - make_describeoptions() + DescribeOptions() .beforeEach([](){}) .afterEach([](){}) .beforeAll([](){}) @@ -92,118 +94,23 @@ testresults_t test_ClassToTest_2(const vector& argv) { }) ); } - -template -class Expectable { - public: - Expectable(const T& actual); - virtual ~Expectable(); - virtual void toEqual(const T& value); - virtual void toNotEqual(const T& value); - // Maybe these funcs should return tuple instead. - virtual void toBe(std::function>(const T& actual)> evaluator); - //virtual void toBeNull(); - //virtual void toNotBeNull(); - //virtual void toThrow(...); - private: - const T& actual_; -}; - -template -Expectable::Expectable(const T& actual) -: actual_(actual) {} - -template -Expectable::~Expectable() {} - -class FailedExpectation: std::runtime_error { - public: - FailedExpectation(const std::string& message); - virtual ~FailedExpectation(); -}; - -FailedExpectation::FailedExpectation(const std::string& message) -: runtime_error(message) { -} - -FailedExpectation::~FailedExpectation() {} - -template -void Expectable::toEqual(const T& value) { - if (actual_ != value) { - std::ostringstream os; - os << "Expected: " << actual_ << " to be " << value; - throw(FailedExpectation(os.str())); - } -} - -template -void Expectable::toNotEqual(const T& value) { - if (actual_ == value) { - std::ostringstream os; - os << "Expected: " << actual_ << " to not be " << value; - throw(FailedExpectation(os.str())); - } -} - -template -void Expectable::toBe(std::function>(const T& actual)> evaluator) { - auto result = evaluator(actual_); - if (!std::get<0>(result)) { - std::ostringstream os; - // std::optional message = std::get<1>; - std::optional message = std::get<1>(result); - if (message.has_value()) { - os << "Expected: " << actual_ << " to pass validation, but " << message.value(); - } else { - os << "Expected: " << actual_ << " to pass validation."; - } - throw(FailedExpectation(os.str())); - } -} - -// template -// void Expectable::toBeNull() { -// if (actual_ != nullptr) { -// std::ostringstream os; -// os << "Expected null, but got " << actual_; -// throw(FailedExpectation(os.str())); -// } -// } - -// template -// void Expectable::toNotBeNull() { -// if (actual_ == nullptr) { -// throw(FailedExpectation("Expected non-null, but got null")); -// } -// } - -// template -// void Expectable::toThrow(...) {} - -template -Expectable expect(const T& actual); - -template -Expectable expect(const T& actual) { - return Expectable(actual); -} int MyAddFunction(int a, int b) { - return 0; + return a+b; } -testresults_t test_BasicExpectable_main(const vector& argv) { +TestResults test_BasicExpectable_main(const vector& argv) { return execute( describe("MyAddFunction", [](){ - return make_testbundle({ + return TestBundle({ it("should add 2 and 2 to get 4", [](){ expect(MyAddFunction(2, 2)).toEqual(4); }), - it("should ", [](){ + it("should fail", [](){ + fail("because I said so"); // Throw exception if somethings goes wrong }), - }, make_describeoptions()); + }, DescribeOptions()); })); } @@ -211,12 +118,15 @@ testresults_t test_BasicExpectable_main(const vector& argv) { int main(int argc, char* argv[]) { try { vector args; - testresults_t results; + TestResults results; - results = add(results, test_ClassToTest_main(args)); - results = add(results, test_BasicExpectable_main(args)); + results += test_ClassToTest_main(args); + // results += test_BasicExpectable_main(args); + // results += test_ClassToTest_2(args); + // results += test_something(args); + // results += test_temp(args); - print_test_results(results, cout); + cout << results << endl; } catch (std::runtime_error ex) { std::cout << "Unhandled exception: " << ex.what() << endl; diff --git a/examples/example.o b/examples/example.o deleted file mode 100644 index 9945706488b5f84b0fdf64fc3c3aac4a2314b9f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9672 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 +#include +namespace JTest { + typedef std::function configure_fn; + typedef std::optional maybe_configure_fn; + maybe_configure_fn combine(maybe_configure_fn first, maybe_configure_fn second); +} +#endif diff --git a/include/JTest/DescribeOptions.h b/include/JTest/DescribeOptions.h new file mode 100644 index 0000000..3bfd70d --- /dev/null +++ b/include/JTest/DescribeOptions.h @@ -0,0 +1,29 @@ +#ifndef JTEST__DESCRIBEOPTIONS_H__ +#define JTEST__DESCRIBEOPTIONS_H__ +namespace JTest { + class DescribeOptions; +} +#include +#include + +//TODO: Add operator<< and maybe print(ostream&) +namespace JTest { + class DescribeOptions { + public: + DescribeOptions(); + maybe_configure_fn getAfterAll() const; + maybe_configure_fn getAfterEach() const; + maybe_configure_fn getBeforeAll() const; + maybe_configure_fn getBeforeEach() const; + DescribeOptions& afterAll(maybe_configure_fn fn); + DescribeOptions& afterEach(maybe_configure_fn fn); + DescribeOptions& beforeAll(maybe_configure_fn fn); + DescribeOptions& beforeEach(maybe_configure_fn fn); + private: + maybe_configure_fn afterAll_; + maybe_configure_fn afterEach_; + maybe_configure_fn beforeAll_; + maybe_configure_fn beforeEach_; + }; +} +#endif diff --git a/include/JTest/Expectable.h b/include/JTest/Expectable.h new file mode 100644 index 0000000..84a580f --- /dev/null +++ b/include/JTest/Expectable.h @@ -0,0 +1,259 @@ +#ifndef JTEST__EXPECTABLE_H__ +#define JTEST__EXPECTABLE_H__ +namespace JTest { + template + class Expectable; +} +#include +#include +#include +#include +#include +#include +#include + +namespace JTest { + template + class Expectable { + public: + Expectable(const T& actual) + : actual_(actual) + , is_inverted_(false) {} + + Expectable(const T& actual, bool is_inverted) + : actual_(actual) + , is_inverted_(is_inverted) {} + + virtual ~Expectable() {} + + const T& actual() { + return actual_; + } + + bool isInverted() { + return is_inverted_; + } + + virtual Expectable nevermore() { + return Expectable(actual_, !is_inverted_); + } + + void toBe(std::function(const T&)> matcher) { + auto reason = matcher(actual_); + if (is_false(reason.has_value())) { + throw FailedExpectation(make_failure_message(*reason)); + } + } + + template + typename std::enable_if::value, void>::type + toBeFalse() { + if (is_true(actual_)) { + throw FailedExpectation(make_failure_message("to be false")); + } + } + + template + typename std::enable_if::value, void>::type + toBeNull() { + if (is_false(actual_ == nullptr)) { + throw FailedExpectation(make_failure_message("to be null")); + } + } + + template + typename std::enable_if::value, bool>::type + toBeTrue() { + if (is_false(actual_)) { + throw FailedExpectation(make_failure_message("to be true")); + } + } + + template + typename std::enable_if>()(std::declval(), std::declval()), void(), std::true_type())::type::value, void>::type + toEqual(const T& value) { + if (is_false(actual_ == value)) { + throw FailedExpectation(make_failure_message("to equal", value)); + } + } + + template + typename std::enable_if::value, bool>::type + toThrow() { + bool has_failed; + std::string reason; + try { + // TODO: Do some magic here to allow for args and member functions. + std::invoke(actual_); + has_failed = is_false(); + if (has_failed) { + reason = "to throw, but did not"; + } + } catch(...) { + has_failed = is_true(); + if (has_failed) { + reason = "to throw, but threw something"; + } + } + if (has_failed) { + throw FailedExpectation(make_failure_message(reason)); + } + } + + template + typename std::enable_if::value, bool>::type + toThrow(const std::exception& expected) { + bool has_failed; + std::string reason = ""; + try { + // TODO: Do some magic here to allow for args and member functions. + std::invoke(actual_); + has_failed = is_false(); + if (has_failed) { + reason = ", but did not throw"; + } + } catch(const std::exception& actual) { + has_failed = is_true(strcmp(expected.what(), actual.what()) == 0); + if (has_failed) { + throw FailedExpectation(make_failure_message("to throw", expected.what(), actual.what())); + reason = string(is_inverted_ ? "" : ", but threw ") + actual.what(); + } + } catch(...) { + has_failed = is_false(); + reason = "but threw something else"; + reason = is_inverted_ ? "" : ", but threw something else"; + } + if (has_failed) { + throw FailedExpectation(make_failure_message("to throw " + (expected.what() + reason))); + } + } + + template + typename std::enable_if::value, bool>::type + toThrow(std::function(const std::exception&)> matcher) { + bool has_failed; + std::optional reason = std::nullopt; + try { + // TODO: Do some magic here to allow for args and member functions. + std::invoke(actual_); + has_failed = is_false(); + if (has_failed) { + reason = "did not throw"; + } + } catch(const std::exception& actual) { + reason = matcher(actual); + has_failed = is_true(reason.has_value()); + } catch(...) { + has_failed = is_false(); + if (has_failed) { + reason = "threw something else"; + } + } + if (has_failed) { + if (reason.has_value()) { + throw FailedExpectation(make_failure_message(is_inverted_ ? "to throw, and " : "to throw, but " + *reason)); + } + // This should never be hit. + throw FailedExpectation(make_failure_message("to throw")); + } + } + + template + typename std::enable_if::value, bool>::type + toThrow(const std::string& what) { + bool has_failed; + std::string reason; + try { + // TODO: Do some magic here to allow for args and member functions. + std::invoke(actual_); + has_failed = is_false(); + if (has_failed) { + reason = "to throw " + what + ", but did not throw anything"; + } + } catch(const std::exception& actual) { + has_failed = is_true(what == actual.what()); + if (has_failed) { + reason = "to throw " + what + ", but actually threw " + actual.what(); + } + } catch(...) { + has_failed = is_false(); + if (has_failed) { + reason = "to throw " + what + ", but threw something that is not an exception"; + } + } + if (has_failed) { + throw FailedExpectation(make_failure_message(reason)); + } + } + + private: + const T& actual_; + const bool is_inverted_; + bool is_true(bool condition = true) { + return condition != is_inverted_; + } + bool is_false(bool condition = true) { + return condition == is_inverted_; + } + + // std::string make_failure_message(const T& expected, const T& actual) { + // ostringstream os; + // os << "expected: " << value << ", actual: " << actual_; + // return os.str(); + // } + // Failed Expectation: + // to be equal with expected = 12, and actual = 0 + // Failed Expectation: + // not to be equal with expected = 12, and actual = 12 + + std::string make_failure_message(const std::string reason) { + std::ostringstream os; + os << (is_inverted_ ? "not " : "") << reason << " with actual = " << actual_; + return os.str(); + } + + std::string make_failure_message(const std::string reason, const T& expected) { + std::ostringstream os; + os << (is_inverted_ ? "not " : "") << reason << " with expected = " << expected << ", and actual = " << actual_; + return os.str(); + } + + std::string make_failure_message(const std::string reason, std::string expected, std::string actual) { + std::ostringstream os; + os << (is_inverted_ ? "not " : "") << reason + << " with expected message = \"" << expected + << "\", and actual message = \"" << actual_ << "\""; + return os.str(); + } + + // TODO: Fix the make_failure_message methods so they only exist if operator<< is defined for (ostream&, T) + // Maybe have it generate a shorter message. + // Maybe also check for string + T and use that if operator<< does not exist. + // If neither of those exist this should at least say "expected not equal to actual" + // Also this should take is_inverted_ into account. + }; + + // Other possible names since Expectable not() is not an option + // Wanted, but can't have: expect(2+2).not().toBe(5); + // expect(2+2).absolutelyNot().toBe(5); + // expect(2+2).definitelyNot().toBe(5); + // expect(2+2).isNot().toBe(5); + // expect(2+2).isNotGoing().toBe(5); + // expect(2+2).nay().toBe(5); + // expect(2+2).neer().toBe(5); + // expect(2+2).neither().toBe(5); + // expect(2+2).never().toBe(5); + // expect(2+2).nevermore().toBe(5); + // expect(2+2).nix().toBe(5); + // expect(2+2).no().toBe(5); + // expect(2+2).notAtAll().toBe(5); + // expect(2+2).notByAnyMeans().toBe(5); + // expect(2+2).surelyNot().toBe(5); + + // This is for T actual(){...} since Expectable has no default constructor. + // Expectable won't compile no default constructor exists for class "Expectable" + // template + // typename std::enable_if::value, T>::type +} + +#endif diff --git a/include/JTest/FailedExpectation.h b/include/JTest/FailedExpectation.h new file mode 100644 index 0000000..0a29b81 --- /dev/null +++ b/include/JTest/FailedExpectation.h @@ -0,0 +1,16 @@ +#ifndef JTEST__FAILEDEXPECTATION_H__ +#define JTEST__FAILEDEXPECTATION_H__ +namespace JTest { + class FailedExpectation; +} +#include +#include +namespace JTest { + class FailedExpectation: public TestFailedException { + public: + FailedExpectation(const std::string& label); + FailedExpectation(const std::string& label, const std::string& path, const std::string& reason); + virtual ~FailedExpectation(); + }; +} +#endif diff --git a/include/JTest/JTest.h b/include/JTest/JTest.h new file mode 100644 index 0000000..c9c7505 --- /dev/null +++ b/include/JTest/JTest.h @@ -0,0 +1,39 @@ +#ifndef JTEST_H__ +#define JTEST_H__ +namespace JTest { +} +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace JTest { + // Jasmine methods + + // Executes the tests in tests. Possibly in parallel. Will block until all async tests have completed. + TestResults execute(TestBundle tests, const std::string& path=""); + TestResults execute(Test test, const std::string& bundle_label); + + // + TestBundle describe(const std::string& label, const make_testbundle_fn& make_tests, std::optional options = std::nullopt); + TestBundle xdescribe(const std::string& label, const make_testbundle_fn& make_tests, std::optional options = std::nullopt); + + // TODO: Make this return a Test instead. + TestBundle it(const std::string& label, const test_fn& test_method, std::optional options = std::nullopt); + TestBundle xit(const std::string& label, const test_fn& test_method, std::optional options = std::nullopt); + + void fail(const std::string& reason); + void pending(const std::string& reason); + + template + Expectable expect(const T& actual) { + return Expectable(actual); + } +} +#endif \ No newline at end of file diff --git a/include/JTest/Test.h b/include/JTest/Test.h new file mode 100644 index 0000000..51db0e0 --- /dev/null +++ b/include/JTest/Test.h @@ -0,0 +1,17 @@ +#ifndef JTEST__TEST_H__ +#define JTEST__TEST_H__ +namespace JTest { + class Test; +} +#include +#include +namespace JTest { + class Test { + public: + std::string label_; + test_fn test_method_; + bool disabled_; + private: + }; +} +#endif diff --git a/include/JTest/TestBundle.h b/include/JTest/TestBundle.h new file mode 100644 index 0000000..2d21ee3 --- /dev/null +++ b/include/JTest/TestBundle.h @@ -0,0 +1,33 @@ +#ifndef JEST__TESTBUNDLE_H__ +#define JEST__TESTBUNDLE_H__ +#include +namespace JTest { + class TestBundle; + typedef std::function make_testbundle_fn; +} +#include +#include +#include +#include +#include +#include +#include +namespace JTest { + class TestBundle { + public: + TestBundle(const std::vector& bundles, const DescribeOptions& options); + TestBundle(const std::string& label, const std::vector& tests); + std::string fully_qualified_label() const; + std::string label_; + std::string fully_qualified_path_; + std::vector tests_; + std::vector children_; + maybe_configure_fn afterAll_; + maybe_configure_fn afterEach_; + maybe_configure_fn beforeAll_; + maybe_configure_fn beforeEach_; + bool disabled_; + private: + }; +} +#endif diff --git a/include/JTest/TestException.h b/include/JTest/TestException.h new file mode 100644 index 0000000..4ed2edb --- /dev/null +++ b/include/JTest/TestException.h @@ -0,0 +1,28 @@ +#ifndef JTEST__TESTEXCEPTION_H__ +#define JTEST__TESTEXCEPTION_H__ +namespace JTest { + class TestException; +} +#include +#include +#include +namespace JTest { + class TestException: public std::runtime_error { + public: + TestException(const std::string& label, const std::string& path); + TestException(const std::string& label, const std::string& path, const std::string& what); + TestException(const std::string& function, const std::string& file, int line, const std::string& reason); + virtual ~TestException(); + virtual std::string test_label() const; + virtual std::string fully_qualified_test_label() const; + virtual TestStatus status() const; + private: + const std::string test_label_; + const std::string fully_qualified_test_label_; + const std::string function_; + const std::string file_; + const int line_; + const std::string reason_; + }; +} +#endif diff --git a/include/JTest/TestFailedException.h b/include/JTest/TestFailedException.h new file mode 100644 index 0000000..04b84ee --- /dev/null +++ b/include/JTest/TestFailedException.h @@ -0,0 +1,20 @@ +#ifndef JTEST__TESTFAILEDEXCEPTION_H__ +#define JTEST__TESTFAILEDEXCEPTION_H__ +namespace JTest{ + class TestFailedException; +} +#include +#include +#include +namespace JTest{ + class TestFailedException: public TestException { + public: + TestFailedException(std::string label, std::string path, std::string reason); + virtual ~TestFailedException(); + virtual TestStatus status() const; + virtual std::string reason() const; + private: + std::string reason_; + }; +} +#endif diff --git a/include/JTest/TestFunction.h b/include/JTest/TestFunction.h new file mode 100644 index 0000000..814953e --- /dev/null +++ b/include/JTest/TestFunction.h @@ -0,0 +1,7 @@ +#ifndef JTEST__TESTFUNCTION_H__ +#define JTEST__TESTFUNCTION_H__ +#include +namespace JTest { + typedef std::function test_fn; +} +#endif diff --git a/include/JTest/TestOptions.h b/include/JTest/TestOptions.h new file mode 100644 index 0000000..abe2775 --- /dev/null +++ b/include/JTest/TestOptions.h @@ -0,0 +1,12 @@ +#ifndef JTEST__TESTOPTIONS_H__ +#define JTEST__TESTOPTIONS_H__ +namespace JTest { + class TestOptions; +} + +namespace JTest { + class TestOptions { + + }; +} +#endif diff --git a/include/JTest/TestPendingException.h b/include/JTest/TestPendingException.h new file mode 100644 index 0000000..158fa61 --- /dev/null +++ b/include/JTest/TestPendingException.h @@ -0,0 +1,17 @@ +#ifndef JTEST__TESTPENDINGEXCEPTION_H__ +#define JTEST__TESTPENDINGEXCEPTION_H__ +namespace JTest { + class TestPendingException; +} +#include +#include + +namespace JTest { + class TestPendingException: public TestException { + public: + TestPendingException(const std::string& label, const std::string& path, const std::string& reason); + virtual ~TestPendingException(); + virtual TestStatus status() const; + }; +} +#endif diff --git a/include/JTest/TestResults.h b/include/JTest/TestResults.h new file mode 100644 index 0000000..04b51b1 --- /dev/null +++ b/include/JTest/TestResults.h @@ -0,0 +1,28 @@ +#ifndef JTEST_TESTRESULTS_H__ +#define JTEST_TESTRESULTS_H__ +namespace JTest { + class TestResults; +} +#include +#include +namespace JTest { + // TODO: Consider hiding the vectors behind accessor methods void add(...), T get(), vector get(uint32_t index) + class TestResults { + public: + TestResults(); + TestResults(uint32_t total, uint32_t skipped, uint32_t passed, uint32_t failed); + TestResults operator+(const TestResults& right) const; + TestResults& operator+=(const TestResults& right); + friend std::ostream& operator<<(std::ostream& os, const TestResults& results); + // TODO: Make these private, make operator++ increment total, add methods pass, fail, and skip to increment passed, failed, and skipped + uint32_t total; + uint32_t skipped; + uint32_t passed; + uint32_t failed; + private: + // vector errors; + // vector failures; + // vector skipped; + }; +} +#endif diff --git a/include/JTest/TestStatus.h b/include/JTest/TestStatus.h new file mode 100644 index 0000000..d72d626 --- /dev/null +++ b/include/JTest/TestStatus.h @@ -0,0 +1,11 @@ +#ifndef JTEST__TESTSTATUS_H__ +#define JTEST__TESTSTATUS_H__ +namespace JTest { + enum TestStatus { + Unknown, + Passed, + Failed, + Skipped, + }; +} +#endif diff --git a/src/JTest/ConfigureFunction.cpp b/src/JTest/ConfigureFunction.cpp new file mode 100644 index 0000000..d72fed2 --- /dev/null +++ b/src/JTest/ConfigureFunction.cpp @@ -0,0 +1,20 @@ +#include +#include +namespace JTest { + maybe_configure_fn combine(maybe_configure_fn first, maybe_configure_fn second) { + if (first.has_value()) { + auto first_v = first.value(); + if (second.has_value()) { + auto second_v = second.value(); + return [first_v, second_v]() { + first_v(); + second_v(); + }; + } else { + return first; + } + } else { + return second; + } + } +} diff --git a/src/JTest/DescribeOptions.cpp b/src/JTest/DescribeOptions.cpp new file mode 100644 index 0000000..d309504 --- /dev/null +++ b/src/JTest/DescribeOptions.cpp @@ -0,0 +1,44 @@ +#include +namespace JTest { + DescribeOptions::DescribeOptions() + : afterAll_(std::nullopt) + , afterEach_(std::nullopt) + , beforeAll_(std::nullopt) + , beforeEach_(std::nullopt) {} + + maybe_configure_fn DescribeOptions::getAfterAll() const { + return afterAll_; + } + + DescribeOptions& DescribeOptions::afterAll(maybe_configure_fn fn) { + this->afterAll_ = fn; + return *this; + } + + maybe_configure_fn DescribeOptions::getAfterEach() const { + return afterEach_; + } + + DescribeOptions& DescribeOptions::afterEach(maybe_configure_fn fn) { + this->afterEach_ = fn; + return *this; + } + + maybe_configure_fn DescribeOptions::getBeforeAll() const { + return beforeAll_; + } + + DescribeOptions& DescribeOptions::beforeAll(maybe_configure_fn fn) { + this->beforeAll_ = fn; + return *this; + } + + maybe_configure_fn DescribeOptions::getBeforeEach() const { + return beforeEach_; + } + + DescribeOptions& DescribeOptions::beforeEach(maybe_configure_fn fn) { + this->beforeEach_ = fn; + return *this; + } +} diff --git a/src/JTest/FailedExpectation.cpp b/src/JTest/FailedExpectation.cpp new file mode 100644 index 0000000..1a673c9 --- /dev/null +++ b/src/JTest/FailedExpectation.cpp @@ -0,0 +1,11 @@ +#include +namespace JTest{ + FailedExpectation::~FailedExpectation() {} + + FailedExpectation::FailedExpectation(const std::string& reason) + : TestFailedException("", "", reason) {} + + // TODO: Figure out if we can get label and path from here without passing them in. + FailedExpectation::FailedExpectation(const std::string& label, const std::string& path, const std::string& reason) + : TestFailedException(label, path, reason) {} +} diff --git a/src/JTest/JTest.cpp b/src/JTest/JTest.cpp new file mode 100644 index 0000000..5123ff6 --- /dev/null +++ b/src/JTest/JTest.cpp @@ -0,0 +1,201 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace JTest { + using std::ostream; + using std::endl; + using std::vector; + using std::string; + using std::runtime_error; + using std::string; + using std::optional; + using std::cout; + using std::exception; + using std::for_each; + + void throw_unimplemented_function_error(string method_name) { + throw runtime_error("Unimplemented function: " + method_name); + } + + void throw_unimplemented_method_error(string class_name, string method_name) { + throw runtime_error("Unimplemented method in class " + class_name + ": " + method_name); + } + + void throw_unimplemented_feature_error(string feature_name) { + throw runtime_error("Unimplemented feature: " + feature_name); + } + + void print_current_function(ostream & out, string function_name = __builtin_FUNCTION()) { + out << "\033[32m" << function_name << "\033[39m" << endl; + } + + TestBundle it(const string& label, const test_fn& test_method, optional options) { + // TODO: Stop ignoring options. + // TODO: Return a Test instead of a TestBundle. + Test test; + test.disabled_ = false; + test.label_ = label; + test.test_method_ = test_method; + return TestBundle("", {test}); + } + + TestBundle xit(const string& label, const test_fn& test_method, optional options) { + TestBundle bundle = it(label, test_method, options); + bundle.tests_.at(0).disabled_ = true; + return bundle; + } + + TestResults execute(TestBundle bundle, const string& path) { + TestResults results; + // TODO: Set this during bundle creation or parsing. + // bundle.fully_qualified_path_ = path.length() > 0 ? path + "::" : ""; + // cout << "executing TestBundle: " << bundle.fully_qualified_label() << endl; + + if (bundle.disabled_) { + // TODO: Recursively report all tests as skipped. + } else { + try { + // TODO: Try to do this in parallel where possible + if (bundle.beforeAll_.has_value()) { + bundle.beforeAll_.value()(); + } + for_each(bundle.children_.begin(), bundle.children_.end(), [&results, &bundle](TestBundle child) { + // TODO: Find a way to make child tests get our beforeEach and afterEach callbacks without modifying child. + child.beforeEach_ = combine(bundle.beforeEach_, child.beforeEach_); + child.afterEach_ = combine(bundle.afterEach_, child.afterEach_); + child.fully_qualified_path_ = bundle.fully_qualified_label(); + results += execute(child); + }); + // TODO: Consider capturing these callbacks differently. Without the bundle? By value? + for_each(bundle.tests_.begin(), bundle.tests_.end(), [&results, &bundle](Test test) { + if (bundle.beforeEach_.has_value()) { + bundle.beforeEach_.value()(); + } + results += execute(test, bundle.fully_qualified_label()); + if (bundle.afterEach_.has_value()) { + bundle.afterEach_.value()(); + } + }); + if (bundle.afterAll_.has_value()) { + bundle.afterAll_.value()(); + } + } catch(const exception& ex) { + // TODO: This should include the fully qualified test name. + cout << "🔥 Unhandled exception running test bundle: " << bundle.label_ << " what: " << ex.what() << endl; + results.failed++; + } catch(...) { + // TODO: This should include the fully qualified test name. + cout << "🔥 Unhandled exception running test bundle: " << bundle.label_ << endl; + results.failed++; + } + } + + return results; + } + + TestResults execute(Test test, const string& bundle_label) { + // cout << "executing Test:" << endl + // << " bundle_label = " << bundle_label << endl + // << " test.label = " << test.label_ << endl; + TestResults results; + results.total++; + int status = 0; + if (test.disabled_) { + results.skipped++; + } else { + try { + test.test_method_(); + results.passed++; + } catch (const TestPendingException& ex) { + results.skipped++; + // TODO: This should be ex.fully_qualified_test_label() but that isn't set yet. + cout << "🚧 Pending Test: " << bundle_label + "::" + test.label_ << endl; + } catch (const TestFailedException& ex) { + results.failed++; + // TODO: This should be ex.fully_qualified_test_label() but that isn't set yet. + cout << "❌ Failed Test: " << bundle_label + "::" + test.label_ << " reason: " << ex.reason() << endl; + } catch(const exception& ex) { + results.failed++; + // TODO: This should include the fully qualified test name. + cout << "🔥 Unhandled exception running test: " << bundle_label + "::" + test.label_ << " what: " << ex.what() << endl; + } catch(...) { + results.failed++; + // TODO: This should include the fully qualified test name. + cout << "🔥 Unhandled exception running test: " << bundle_label + "::" + test.label_ << endl; + } + // throw; can still escape these catch clauses because it calls std::terminate and we can't catch/interrupt that. + } + return results; + } + + TestBundle describe(const string& label, const make_testbundle_fn& maketests_fn, optional options) { + TestBundle bundle = maketests_fn(); + // vector newChildren; + // for_each(bundle.children_.begin(), bundle.children_.end(), [&bundle, &newChildren](TestBundle& child) { + // if (child.label_.length() == 0 && !child.afterAll_.has_value() && !child.afterEach_.has_value() + // && !child.beforeAll_.has_value() && !child.beforeEach_.has_value()) { + // for_each(child.tests_.begin(), child.tests_.end(), [&bundle](Test test) { + // bundle.tests_.push_back(test); + // }); + // } else { + // newChildren.push_back(bundle); + // } + // }); + // bundle.children_ = newChildren; + + bundle.label_ = label; + bundle.disabled_ = false; + if (options.has_value()) { + DescribeOptions options_v = options.value(); + bundle.afterAll_ = combine(options_v.getAfterAll(), bundle.afterAll_); + bundle.afterEach_ = combine(options_v.getAfterEach(), bundle.afterEach_); + bundle.beforeAll_ = combine(options_v.getBeforeAll(), bundle.beforeAll_); + bundle.beforeEach_ = combine(options_v.getBeforeEach(), bundle.beforeEach_); + } + // TODO: go through all child bundles and tests to update the fully qualified path/label + // cout << "describe: bundle_label = " << bundle.label_ + // << ", bundle.tests.size() = " << bundle.tests_.size() + // << ", bundle.children.size() = " << bundle.children_.size() << endl; + return bundle; + } + + void fail(const std::string& reason) { + // TODO: Figure out how to get label, and path + string label = ""; + string path = ""; + throw TestFailedException(label, path, reason); + } + + void pending(const std::string& reason) { + // TODO: Figure out how to get label, and path + string label = ""; + string path = ""; + throw TestPendingException(label, path, reason); + } + + // TODO: Use these to make the unimplemented_* errors simpler to call. + // For this function + // TestBundle describe(const std::string& label, const make_testbundle_fn& maketests_, std::optional options) + // __PRETTY_FUNCTION__ + // Unimplemented function: JTest::TestBundle JTest::describe(const std::string &, const JTest::make_testbundle_fn &, std::optional) + // __FUNCSIG__ is not defined on clang++ + // __func__ + // describe + // __LINE__ is an integer + // + // __FILE__ + // examples/JSTest.cpp + // __FUNCTION__ + // describe +} diff --git a/JTest.cpp b/src/JTest/Test.cpp similarity index 100% rename from JTest.cpp rename to src/JTest/Test.cpp diff --git a/src/JTest/TestBundle.cpp b/src/JTest/TestBundle.cpp new file mode 100644 index 0000000..3261f53 --- /dev/null +++ b/src/JTest/TestBundle.cpp @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace JTest { + using std::string; + using std::vector; + using std::nullopt; + + TestBundle::TestBundle(const vector& bundles, const DescribeOptions& options) + : label_("") + , fully_qualified_path_("") + , tests_({}) + , children_({}) + , afterAll_(options.getAfterAll()) + , afterEach_(options.getAfterEach()) + , beforeAll_(options.getBeforeAll()) + , beforeEach_(options.getBeforeEach()) + , disabled_(false) { + for_each(bundles.begin(), bundles.end(), [this](TestBundle bundle) { + if (bundle.tests_.size() == 0 && bundle.children_.size() == 1 && bundle.label_.size() == 0) { + tests_.push_back(bundle.tests_.at(0)); + } else { + children_.push_back(bundle); + } + }); + } + + TestBundle::TestBundle(const string& label, const vector& tests) + : label_(label) + , fully_qualified_path_("") + , tests_({}) + , children_({}) + , afterAll_(nullopt) + , afterEach_(nullopt) + , beforeAll_(nullopt) + , beforeEach_(nullopt) + , disabled_(false) { + for_each(tests.begin(), tests.end(), [this](Test test) { + tests_.push_back(test); + }); + } + + string TestBundle::fully_qualified_label() const { + return (fully_qualified_path_.length()>0?fully_qualified_path_ + "::":"") + (label_.length()>0?label_:""); + } +} diff --git a/src/JTest/TestException.cpp b/src/JTest/TestException.cpp new file mode 100644 index 0000000..26637d2 --- /dev/null +++ b/src/JTest/TestException.cpp @@ -0,0 +1,47 @@ +#include +#include + +namespace JTest { + using std::string; + + TestException::TestException(const string& label, const string& path) + : runtime_error("Exception in test: " + path + "::" + label) + , test_label_(label) + , fully_qualified_test_label_(path + "::" + label) + , function_("") + , file_("") + , line_(-1) + , reason_("") {} + + TestException::TestException(const string& label, const string& path, const string& what) + : runtime_error(what) + , test_label_(label) + , fully_qualified_test_label_(path + "::" + label) + , function_("") + , file_("") + , line_(-1) + , reason_("") {} + + TestException::TestException(const std::string& function, const std::string& file, int line, const std::string& reason) + : runtime_error(reason) + , test_label_("") + , fully_qualified_test_label_("") + , function_(function) + , file_(file) + , line_(line) + , reason_(reason) {} + + TestException::~TestException() {} + + string TestException::test_label() const { + return test_label_; + } + + string TestException::fully_qualified_test_label() const { + return fully_qualified_test_label_; + } + + TestStatus TestException::status() const { + return TestStatus::Unknown; + } +} diff --git a/src/JTest/TestFailedException.cpp b/src/JTest/TestFailedException.cpp new file mode 100644 index 0000000..63449cd --- /dev/null +++ b/src/JTest/TestFailedException.cpp @@ -0,0 +1,22 @@ +#include +#include +#include +#include + +namespace JTest { + using std::string; + + TestFailedException::TestFailedException(string label, string path, string reason) + : TestException(label, path, "Failed Test: " +fully_qualified_test_label()) + , reason_(reason) {} + + TestFailedException::~TestFailedException() {} + + TestStatus TestFailedException::status() const { + return TestStatus::Failed; + } + + string TestFailedException::reason() const { + return reason_; + } +} diff --git a/include/JTest.h b/src/JTest/TestFunction.cpp similarity index 100% rename from include/JTest.h rename to src/JTest/TestFunction.cpp diff --git a/src/JTest/TestOptions.cpp b/src/JTest/TestOptions.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/JTest/TestPendingException.cpp b/src/JTest/TestPendingException.cpp new file mode 100644 index 0000000..f05f859 --- /dev/null +++ b/src/JTest/TestPendingException.cpp @@ -0,0 +1,17 @@ +#include +#include +#include +#include + +namespace JTest { + using std::string; + + TestPendingException::TestPendingException(const string& label, const string& path, const string& reason) + : TestException(label, path, "Pending Test: " + fully_qualified_test_label()) {} + + TestPendingException::~TestPendingException() {} + + TestStatus TestPendingException::status() const { + return TestStatus::Skipped; + } +} diff --git a/src/JTest/TestResults.cpp b/src/JTest/TestResults.cpp new file mode 100644 index 0000000..9fe9f8b --- /dev/null +++ b/src/JTest/TestResults.cpp @@ -0,0 +1,35 @@ +#include + +namespace JTest { + TestResults::TestResults() + : total(0) + , skipped(0) + , passed(0) + , failed(0) {} + + TestResults::TestResults(uint32_t total, uint32_t skipped, uint32_t passed, uint32_t failed) + : total(total) + , skipped(skipped) + , passed(passed) + , failed(failed) {} + + TestResults TestResults::operator+(const TestResults& right) const { + return TestResults( + total + right.total, skipped + right.skipped, + passed + right.passed, failed + right.failed); + } + + TestResults& TestResults::operator+=(const TestResults& right) { + total += right.total; + skipped += right.skipped; + passed += right.passed; + failed += right.failed; + return *this; + } + + std::ostream& operator<<(std::ostream& os, const TestResults& results) { + os << "Total: " << results.total << ", Passed: " << results.passed + << ", Failed: " << results.failed << ", Skipped: " << results.skipped; + return os; + } +} diff --git a/src/JTest/TestStatus.cpp b/src/JTest/TestStatus.cpp new file mode 100644 index 0000000..e69de29