diff --git a/test/array-test.cpp b/test/array-test.cpp index 4090a4b..755684b 100644 --- a/test/array-test.cpp +++ b/test/array-test.cpp @@ -16,9 +16,9 @@ #include "../include/array.h" -#include "tester.h" +#include "verifiers.h" -void runTests() +void array_test() { using sigcpp::array; @@ -30,41 +30,41 @@ void runTests() //capacity - verify(!s.empty(), "s.empty()"); - verify(s.size() == 3, "s.size()"); - verify(s.max_size() == s.size(), "s.max_size()"); + is_false(s.empty(), "s.empty()"); + is_true(s.size() == 3, "s.size()"); + is_true(s.max_size() == s.size(), "s.max_size()"); - verify(!p.empty(), "p.empty()"); - verify(p.size() == 5, "p.size()"); - verify(p.max_size() == p.size(), "p.max_size()"); + is_false(p.empty(), "p.empty()"); + is_true(p.size() == 5, "p.size()"); + is_true(p.max_size() == p.size(), "p.max_size()"); //element access - verify(s[0] == 8, "s[0]"); - verify(s[1] == -2, "s[1]"); - verify(s[2] == 7, "s[2]"); - verify(s[1] != 8, "s[1] != 8"); + is_true(s[0] == 8, "s[0]"); + is_true(s[1] == -2, "s[1]"); + is_true(s[2] == 7, "s[2]"); + is_true(s[1] != 8, "s[1] != 8"); - verify(p[0] == 8, "p[0]"); - verify(p[2] == 7, "p[2]"); - verify(p[4] == 0, "p[4]"); + is_true(p[0] == 8, "p[0]"); + is_true(p[2] == 7, "p[2]"); + is_true(p[4] == 0, "p[4]"); - verify(s.at(0) == 8, "s.at(0)"); - verify(s.at(1) == -2, "s.at(1)"); - verify(s.at(2) == 7, "s.at(2)"); + is_true(s.at(0) == 8, "s.at(0)"); + is_true(s.at(1) == -2, "s.at(1)"); + is_true(s.at(2) == 7, "s.at(2)"); - verify(p.at(0) == 8, "p.at(0)"); - verify(p.at(2) == 7, "p.at(2)"); - verify(p.at(4) == 0, "p.at(4)"); + is_true(p.at(0) == 8, "p.at(0)"); + is_true(p.at(2) == 7, "p.at(2)"); + is_true(p.at(4) == 0, "p.at(4)"); - verify(s.front() == 8, "s.front()"); - verify(s.front() != -2, "s.front() != -2"); - verify(s.back() == 7, "s.back()"); - verify(s.back() != -2, "s.back() != -2"); + is_true(s.front() == 8, "s.front()"); + is_true(s.front() != -2, "s.front() != -2"); + is_true(s.back() == 7, "s.back()"); + is_true(s.back() != -2, "s.back() != -2"); - verify(p.front() == 8, "p.front()"); - verify(p.front() != -2, "p.front() != -2"); - verify(p.back() == 0, "p.back()"); + is_true(p.front() == 8, "p.front()"); + is_true(p.front() != -2, "p.front() != -2"); + is_true(p.back() == 0, "p.back()"); //forward iterators @@ -75,7 +75,7 @@ void runTests() std::size_t i = 0; for (auto it = u.begin(); it != u.end() && iteratorTest; ++it, ++i) iteratorTest = *it == uExpected[i]; - verify(iteratorTest, "forward iterator"); + is_true(iteratorTest, "forward iterator"); //reverse iterators unsigned urExpected[] = { 6, 1, 3, 9, 5 }; @@ -84,17 +84,17 @@ void runTests() i = 0; for (auto it = u.rbegin(); it != u.rend() && iteratorTest; ++it, ++i) iteratorTest = *it == urExpected[i]; - verify(iteratorTest, "reverse iterator"); + is_true(iteratorTest, "reverse iterator"); //zero-size array array c; - verify(c.empty(), "c.empty()"); + is_true(c.empty(), "c.empty()"); //iterator on empty array: the loop body should not execute iteratorTest = true; for (const auto e : c) iteratorTest = false; - verify(iteratorTest, "fwd iterator on empty array"); + is_true(iteratorTest, "fwd iterator on empty array"); //fill @@ -106,7 +106,7 @@ void runTests() bool fillTest = !std::any_of(a.begin(), a.end(), [](char c) { return c != 'x'; } ); - verify(fillTest, "a.fill()"); + is_true(fillTest, "a.fill()"); //swap @@ -120,5 +120,5 @@ void runTests() bool swapTest = true; for (std::size_t idx = 0; idx < m.size() && swapTest; ++idx) swapTest = m[idx] == mExpected[idx] && n[idx] == nExpected[idx]; - verify(swapTest, "m.swap(n)"); -} \ No newline at end of file + is_true(swapTest, "m.swap(n)"); +} diff --git a/test/driver.cpp b/test/driver.cpp index 63e6346..d151e56 100644 --- a/test/driver.cpp +++ b/test/driver.cpp @@ -16,18 +16,33 @@ #include #include #include +#include +#include +#include +#include "utils.h" +#include "suites.h" #include "tester.h" #include "options.h" #include "options-exceptions.h" -//must be defined in a unit-specific source file such as "array-test.cpp" -void runTests(); +//error codes returned back from main are negative +enum class error_code { + cmd_line_initial = -1, file_initial = -2, unexpected_typed_initial = -3, unexpected_untyped_initial = -4, + cmd_line_run = -6, file_run = -7, suite_add_run = -8, unexpected_typed_run = -9, + unexpected_untyped_run = -10 +}; -static void show_error(const char* message); + +void run_suites(const Options& options); +static int show_error(const char* message, error_code ec); static void show_usage(const char* program_path); -static void show_error_and_usage(const char* message, const char* program_path); +static int show_error_and_usage(const char* message, const char* program_path, error_code ec); + +//return < 0: error +//return == 0: no error, no tests failed +//return > 0: no error, tests failed; number of failed tests returned int main(int argc, char* argv[]) { Options options; @@ -38,59 +53,107 @@ int main(int argc, char* argv[]) apply_options(options, fileOut); } catch (const cmd_line_error& cle) { - show_error_and_usage(cle.what(), argv[0]); - return -1; + return show_error_and_usage(cle.what(), argv[0], error_code::cmd_line_initial); } catch (const file_error& fe) { - show_error_and_usage(fe.what(), argv[0]); - return -2; + return show_error_and_usage(fe.what(), argv[0], error_code::file_initial); } catch (const std::exception& e) { - show_error((std::string{ "Unexpected error: " } +e.what()).data()); - return -3; + auto message = format_message("Unexpected error", e.what()); + return show_error(message.data(), error_code::unexpected_typed_initial); } catch (...) { - show_error("Unexpected error"); - return -4; + show_error("Unexpected error", error_code::unexpected_untyped_initial); } try { - runTests(); + run_suites(options); + } + catch (const cmd_line_error& cle) { + show_error_and_usage(cle.what(), argv[0], error_code::cmd_line_run); } - catch (const std::string& msg) { - logLine(msg.data()); - if (options.fom == file_open_mode::no_file) - show_error(msg.data()); + catch (const test_suite_add_error& tae) { + show_error(tae.what(), error_code::suite_add_run); + } + catch (const std::exception& e) { + auto message = format_message("Unexpected error", e.what()); + show_error(message.data(), error_code::unexpected_typed_run); + } + catch (...) { + show_error("Unexpected error", error_code::unexpected_untyped_run); } if (options.summary) - summarizeTests(); + summarize_tests(); - return getTestsFailed(); + return get_tests_failed_total(); +} + + +//run test suites in sequence: runs all suites defined or only those whose names are specified in options +void run_suites(const Options& options) +{ + //retrieve all test suites defined + auto suites = get_test_suites(); + + //build a collection of suite names specified in the options structucre + //options.suites_to_run is empty or a semi-colon delimited list of suite names + const auto names_to_run = split(options.suites_to_run, ';'); + auto run_all_suites = names_to_run.empty(); + + //check that the suite names specified in options correspond to suites defined + //this check is not required to run the suites, but is included to inform the user of any issues + //silently ignoring an unfound suite leaves the user unaware of the reason the suite doesn't run + if (!run_all_suites) { + auto end_suites = suites.cend(); + for (auto& suite_name : names_to_run) { + if (suites.find(suite_name) == end_suites) { + assert(false); + throw invalid_option_value{ std::string{"test suite "} +suite_name + " not defined" }; + } + } + } + + //run all suites or only the suites indicated in options + auto size = suites.size(); + auto begin_names_to_run = names_to_run.cbegin(), end_names_to_run = names_to_run.cend(); + for (const auto& suite : suites) { + const auto& key = suite.first; + if (run_all_suites || std::find(begin_names_to_run, end_names_to_run, key) != end_names_to_run) { + start_suite(key); + suite.second(); + if (size > 1 && options.summary) + summarize_suite(); + } + } } //the following functions assume they are called only from main so that the parameters are always correct //assertion and error handling are included by design -static void show_error_and_usage(const char* message, const char* program_path) +static int show_error_and_usage(const char* message, const char* program_path, error_code ec) { - show_error(message); + show_error(message, ec); std::cout << '\n'; show_usage(program_path); + + return static_cast(ec); } -static void show_error(const char* message) +static int show_error(const char* message, error_code ec) { - std::cout << message << '\n'; + int code{ static_cast(ec) }; + std::cout << "Error " << code << "; " << message << '\n'; + return code; } static void show_usage(const char* program_path) { - std::string program_filename = std::filesystem::path{ program_path }.filename().string(); + auto program_filename = std::filesystem::path{ program_path }.filename().string(); std::cout << "Usage: " << program_filename << " {option_name option_value}\n\n"; @@ -104,14 +167,15 @@ static void show_usage(const char* program_path) "Angle brackets are placeholders for option values:\n\n"; std::cout << - " -h \n" - " -ht
\n" - " -s \n" - " -p \n" - " -t \n" - " -fn \n" - " -fo \n" - " -fa \n" + " -h \n" + " -ht
\n" + " -s \n" + " -p \n" + " -t \n" + " -run \n" + " -fn \n" + " -fo \n" + " -fa \n" "\n"; std::cout << diff --git a/test/options-exceptions.h b/test/options-exceptions.h index e695103..3db5600 100644 --- a/test/options-exceptions.h +++ b/test/options-exceptions.h @@ -19,23 +19,16 @@ #include #include -static std::string message(const std::string_view& base, const std::string& extra = "") -{ - std::string msg{ base }; - if (!extra.empty()) - msg += ": " + extra; - - return msg; -} +#include "utils.h" //base class for cmd-line errors: no public ctors to force use of a specialized error class cmd_line_error : public std::runtime_error { protected: - cmd_line_error(const std::string_view& base) : std::runtime_error{ message(base) } {} + cmd_line_error(const std::string_view& base) : std::runtime_error{ format_message(base) } {} cmd_line_error(const std::string_view& base, const std::string& details) : - std::runtime_error{ message(base, details) }, + std::runtime_error{ format_message(base, details) }, details_{ details } {} const std::string& details() const noexcept @@ -118,10 +111,10 @@ class invalid_option_value : public cmd_line_error { class file_error : public std::runtime_error { public: - file_error(const std::string& base) : std::runtime_error{ message(base) } {} + file_error(const std::string& base) : std::runtime_error{ format_message(base) } {} file_error(const std::string& base, const std::filesystem::path& filepath) : - std::runtime_error{ message(base, filepath.string()) }, + std::runtime_error{ format_message(base, filepath.string()) }, filepath_{ filepath } {} const std::filesystem::path& filepath() const noexcept diff --git a/test/options.cpp b/test/options.cpp index 3bf23c3..0801504 100644 --- a/test/options.cpp +++ b/test/options.cpp @@ -23,6 +23,7 @@ #include "options.h" #include "options-exceptions.h" +#include "utils.h" Options get_options(char* arguments[], const std::size_t size) { @@ -51,7 +52,7 @@ Options get_options(char* arguments[], const std::size_t size) //names in name-value pair for cmd-line options constexpr std::string_view option_name_header{ "-h" }, option_name_header_text{ "-ht" }, option_name_summary{ "-s" }, option_name_prm{ "-p" }, option_name_threshold{ "-t" }, - option_name_file_start{ "-f" }; + option_name_file_start{ "-f" }, option_name_run{ "-run" }; std::string_view prm_value; std::string output_filepath_value; @@ -76,12 +77,14 @@ Options get_options(char* arguments[], const std::size_t size) options.header = strtobool(value); else if (name == option_name_header_text) options.header_text = value; - else if (name == option_name_prm) - prm_value = value; //delay converting prm to enum until after file open mode is known else if (name == option_name_summary) options.summary = strtobool(value); + else if (name == option_name_prm) + prm_value = value; //delay converting prm to enum until after file open mode is known else if (name == option_name_threshold) options.fail_threshold = get_fail_threshold(value); + else if (name == option_name_run) + options.suites_to_run = value; else if (name._Starts_with(option_name_file_start)) { options.fom = get_file_open_mode(name); output_filepath_value = value; @@ -116,16 +119,16 @@ Options get_options(char* arguments[], const std::size_t size) void apply_options(const Options& options, std::ofstream& fileOut) { - setHeaderText(options.header_text); - setPassReportMode(options.prm); - setFailThreshold(options.fail_threshold); + set_header_text(options.header_text); + set_pass_report_mode(options.prm); + set_fail_threshold(options.fail_threshold); //if output to file option not enabled, use standard output //else open output file in appropriate mode if (options.fom == file_open_mode::no_file) - setOutput(std::cout); + set_output(std::cout); else { - setOutput(fileOut); + set_output(fileOut); //enforce create-only file open mode if (options.fom == file_open_mode::new_file && std::filesystem::exists(options.output_filepath)) { @@ -219,8 +222,8 @@ unsigned short get_fail_threshold(const std::string_view& sv) auto begin = sv.data(), end = begin + sv.size(); auto result = std::from_chars(begin, end, value); - //check for "out of range" - bool success = result.ec == std::errc(); + //check for conversion errors + auto success = result.ec == std::errc(); assert(success); if (!success) throw invalid_option_value{ sv }; @@ -233,13 +236,3 @@ unsigned short get_fail_threshold(const std::string_view& sv) return value; } - - -//replace all instances of a substring with a new substring -void replace_all(std::string& str, const std::string& substr, const std::string& new_substr) -{ - auto pos = str.find(substr); - auto substr_size = substr.size(), new_substr_size = new_substr.size(); - for (; pos != std::string::npos; pos = str.find(substr, pos + new_substr_size)) - str.replace(pos, substr_size, new_substr); -} diff --git a/test/options.h b/test/options.h index e3a10df..626eb7e 100644 --- a/test/options.h +++ b/test/options.h @@ -24,12 +24,13 @@ enum class file_open_mode { no_file, new_file, overwrite, append }; struct Options { bool header{ true }; bool summary{ true }; - std::string header_text{ "Running $cmd" }; + std::string header_text{ "Running $suite" }; pass_report_mode prm{ pass_report_mode::indicate }; - unsigned short fail_threshold = 0; + unsigned short fail_threshold{ 0 }; file_open_mode fom{ file_open_mode::no_file }; std::filesystem::path output_filepath; std::string command_name; + std::string suites_to_run; }; @@ -45,6 +46,4 @@ unsigned short get_fail_threshold(const std::string_view& value); bool strtobool(const std::string_view& value); -void replace_all(std::string& str, const std::string& substr, const std::string& new_substr); - -#endif \ No newline at end of file +#endif diff --git a/test/suites.cpp b/test/suites.cpp new file mode 100644 index 0000000..2633fd3 --- /dev/null +++ b/test/suites.cpp @@ -0,0 +1,93 @@ +/* +* suites.cpp +* Sean Murthy +* (c) 2020 sigcpp https://sigcpp.github.io. See LICENSE.MD +* +* Attribution and copyright notice must be retained. +* - Attribution may be augmented to include additional authors +* - Copyright notice cannot be altered +* Attribution and copyright info may be relocated but they must be conspicuous. +* +* Define a collection of test suites and functions to manage the collection +* Edit this file with a lot of care: many carefully designed macros to make suite definition easy and safe +* Go to the section at the end of this file to add a test suite +*/ + +#include +#include + +#include "suites.h" + +//keyed collection of all test suites defined +static suites_map_type test_suites; + +//add an entry to the suites collection, throwing an appropriate exception if insertion fails +static void insert_suite(suites_map_type& suites, const std::string& name, suite_runner_type runner) +{ + if (name.empty()) + throw test_suite_add_error("empty test-suite name", ""); + else if (!suites.insert({ name, runner }).second) { + if (suites.find(name) == suites.end()) + throw test_suite_add_error("unknown error adding test suite", name); + else + throw test_suite_add_error("duplicate test-suite name", name); + } +} + +//macros to facilitate 1-step definition and addition of suite runners to suites collection +//TEST_SUITE is the only macro necessary, but the other macros are defined and used so that +//the section where test suites are added looks declarative and cohesive + +#define DECLARE_BUILD_SUITES_COLLECTION void build_suites_collection(); + +#define BUILD_SUITES_COLLECTION \ + if (collection_not_built) \ + build_suites_collection(); + +#define START_SUITES_COLLECTION \ +void build_suites_collection() \ +{ + +#define END_SUITES_COLLECTION \ +} + + +//macro to declare a suite runner function and to add it to the suites collection +//Example: TEST_SUITE(array_test) adds the following two lines of code: +// void array_test(); +// insert_suite(test_suites, "array_test", array_test; +#define TEST_SUITE(SUITE_NAME) \ + void SUITE_NAME(); \ + insert_suite(test_suites, #SUITE_NAME, SUITE_NAME); + + +//flag to denote if suites collection is already built +static bool collection_not_built{ true }; + + +//provide read-only access to the collection of all test suites defined +//build collection "just in time" if it has not already been built +const std::unordered_map& get_test_suites() +{ + DECLARE_BUILD_SUITES_COLLECTION; // void build_suites_collection(); + BUILD_SUITES_COLLECTION; // if (collection_not_built) build_suites_collection(); + + collection_not_built = false; + return test_suites; +} + + +//this section is intentionally placed at the end of file to make it easier and safer to add test suites +//the collection is as good as being built with a initializer-list ctor, but the technique used here provides +//a 1-step approach instead of the 2-step approach that would be necessary if initializer-list ctor is used +//(2 steps: declare suite runner and add suite runner to the collection) + +START_SUITES_COLLECTION //same as void build_suites_collection() { + + //add one line per test suite: macro parameter should be the name of a suite-runner function + //the semi-colon at the end of macro invocation is not required but its use make things look "authentic" + + TEST_SUITE(array_test); + +// do not add/edit anything after this line +END_SUITES_COLLECTION // same as } diff --git a/test/suites.h b/test/suites.h new file mode 100644 index 0000000..1392774 --- /dev/null +++ b/test/suites.h @@ -0,0 +1,46 @@ +/* +* suites.h +* Sean Murthy +* (c) 2020 sigcpp https://sigcpp.github.io. See LICENSE.MD +* +* Attribution and copyright notice must be retained. +* - Attribution may be augmented to include additional authors +* - Copyright notice cannot be altered +* Attribution and copyright info may be relocated but they must be conspicuous. +* +* Declare types and functions to access collection of test suites +*/ + +#ifndef STL_LITE_SUITES_H +#define STL_LITE_SUITES_H + +#include +#include +#include + +#include "utils.h" + +using suite_runner_type = void(*)(); +using suites_map_type = std::unordered_map; + +const suites_map_type& get_test_suites(); + + +//error when adding a test suite to collection of suites +class test_suite_add_error : public std::runtime_error { + +public: + test_suite_add_error(const std::string& base, const std::string& suite_name) + : std::runtime_error{ format_message(base, suite_name) }, suite_name_{ suite_name } {} + + const std::string& suite_name() const noexcept + { + return suite_name_; + } + +private: + std::string suite_name_; +}; + + +#endif diff --git a/test/test-stl-lite.vcxproj b/test/test-stl-lite.vcxproj index 4686419..2243c1e 100644 --- a/test/test-stl-lite.vcxproj +++ b/test/test-stl-lite.vcxproj @@ -162,12 +162,17 @@ + + + + + diff --git a/test/test-stl-lite.vcxproj.filters b/test/test-stl-lite.vcxproj.filters index d2fc38c..1d63bbe 100644 --- a/test/test-stl-lite.vcxproj.filters +++ b/test/test-stl-lite.vcxproj.filters @@ -27,6 +27,12 @@ Source Files + + Source Files + + + Source Files + @@ -38,5 +44,14 @@ Header Files + + Header Files + + + Header Files + + + Header Files + \ No newline at end of file diff --git a/test/tester.cpp b/test/tester.cpp index 0fd8c28..bf8b291 100644 --- a/test/tester.cpp +++ b/test/tester.cpp @@ -16,122 +16,160 @@ #include #include +#include "utils.h" #include "tester.h" -static std::string headerText("Running tests"); -void setHeaderText(std::string text) +static std::string headerText("Running $suite:"); +void set_header_text(std::string text) { headerText = text; } -static pass_report_mode passMode{ pass_report_mode::indicate }; -void setPassReportMode(pass_report_mode mode) +static pass_report_mode prm{ pass_report_mode::indicate }; +void set_pass_report_mode(pass_report_mode mode) { - passMode = mode; + prm = mode; } -static unsigned short failThreshold{ 0 }; -void setFailThreshold(unsigned short value) +static unsigned short fail_threshold{ 0 }; +void set_fail_threshold(unsigned short value) { - failThreshold = value; + fail_threshold = value; } -void setMaxFailThreshold() +void set_max_fail_threshold() { - failThreshold = USHRT_MAX; + fail_threshold = USHRT_MAX; } static std::ostream* pOut{ &std::cout }; -void setOutput(std::ostream& o) +void set_output(std::ostream& o) { pOut = &o; } -//number of tests run and failed -static unsigned testsDone; -unsigned getTestsDone() +void log(const char* s) { - return testsDone; + *pOut << s; } -static unsigned testsFailed; -unsigned getTestsFailed() +void log_line(const char* s) { - return testsFailed; + *pOut << s << '\n'; } -bool lastOutputEndedInLineBreak{ false }; +static unsigned tests_done_total; +static unsigned tests_done_suite; +static unsigned tests_failed_total; +static unsigned tests_failed_suite; -//track number of tests and check test result -void verify(bool success, const char* hint) +unsigned get_tests_failed_total() { - ++testsDone; - if (testsDone == 1 && !headerText.empty()) - *pOut << headerText << ":\n"; - - std::ostringstream message; + return tests_failed_total; +} - //assume stream is at start of line on first call - lastOutputEndedInLineBreak = testsDone == 1; - if (success) { - if (passMode == pass_report_mode::indicate) - message << '.'; - else if (passMode == pass_report_mode::detail) { - message << "Test# " << testsDone << ": Pass (" << hint << ")\n"; - lastOutputEndedInLineBreak = true; - } +static std::string suite_name; +static unsigned suites_run; +void start_suite(const std::string& name) +{ + suite_name = name; + ++suites_run; + + //print a separator between test suites + if (tests_done_suite != 0) + *pOut << "\n\n"; + + tests_done_suite = 0; + tests_failed_suite = 0; + + //print header text after expanding macro $suite + if (!headerText.empty()) { + const std::string suite_macro{ "$suite" }; + std::string suite_header{ headerText }; + replace_all(suite_header, "$suite", name); + *pOut << suite_header << ":\n"; } - else { - ++testsFailed; +} - if (!lastOutputEndedInLineBreak) - message << '\n'; - message << "Test# " << testsDone << ": FAIL (" << hint << ")\n"; - lastOutputEndedInLineBreak = true; - if (testsFailed > failThreshold) - throw message.str(); - } +//helper for formatting output +bool last_output_ended_in_linebreak{ false }; - *pOut << message.str(); + +//print a report for the suite +void summarize_suite() +{ + if (!last_output_ended_in_linebreak) + *pOut << "\n\n"; + + *pOut << "Tests completed: " << tests_done_suite << '\n'; + *pOut << "Tests passed: " << tests_done_suite - tests_failed_suite << '\n'; + *pOut << "Tests failed: " << tests_failed_suite << '\n'; + + last_output_ended_in_linebreak = true; } -//print a simple test report -void summarizeTests() +//print a report across all suites +void summarize_tests() { //TODO: there should always be one and exactly one empty line before summary //-assume if an exception was thrown earlier on test failure, the client //-printed the msg and caused a line break after printing the msg - if (!lastOutputEndedInLineBreak) + if (!last_output_ended_in_linebreak) *pOut << '\n'; - else if (testsFailed <= failThreshold) + else if (tests_failed_total <= fail_threshold) *pOut << '\n'; - *pOut << "Tests completed: " << testsDone << '\n'; - *pOut << "Tests passed: " << testsDone - testsFailed << '\n'; - *pOut << "Tests failed: " << testsFailed << '\n'; + *pOut << "Suites run: " << suites_run << '\n'; + *pOut << "Tests completed: " << tests_done_total << '\n'; + *pOut << "Tests passed: " << tests_done_total - tests_failed_total << '\n'; + *pOut << "Tests failed: " << tests_failed_total << '\n'; - if (testsFailed > failThreshold) - *pOut << "Tests stopped after " << testsFailed << " failure(s)\n"; + if (tests_failed_total > fail_threshold) + *pOut << "Tests stopped after " << tests_failed_total << " failure(s)\n"; } -void log(const char* s) +//track number of tests and check test result +void verify(bool success, const char* hint) { - *pOut << s; -} + ++tests_done_total; + ++tests_done_suite; + //assume stream is at start of line on first call + last_output_ended_in_linebreak = tests_done_suite == 1; -void logLine(const char* s) -{ - *pOut << s << '\n'; + std::ostringstream message; + + if (success) { + if (prm == pass_report_mode::indicate) + message << '.'; + else if (prm == pass_report_mode::detail) { + message << "Test# " << tests_done_suite << ": Pass (" << hint << ")\n"; + last_output_ended_in_linebreak = true; + } + } + else { + ++tests_failed_total; + ++tests_failed_suite; + + if (!last_output_ended_in_linebreak) + message << '\n'; + message << "Test# " << tests_done_suite << ": FAIL (" << hint << ")\n"; + last_output_ended_in_linebreak = true; + + if (tests_failed_total > fail_threshold) + throw message.str(); + } + + *pOut << message.str(); } diff --git a/test/tester.h b/test/tester.h index f736d9f..d90f6b6 100644 --- a/test/tester.h +++ b/test/tester.h @@ -15,22 +15,26 @@ #define STL_LITE_TESTER_H #include +#include enum class pass_report_mode { none, indicate, detail }; -void setHeaderText(std::string text); -void setPassReportMode(pass_report_mode mode); -void setFailThreshold(unsigned short value); -void setMaxFailThreshold(); -void setOutput(std::ostream& o); +void set_header_text(std::string text); +void set_pass_report_mode(pass_report_mode mode); +void set_fail_threshold(unsigned short value); +void set_max_fail_threshold(); +void set_output(std::ostream& o); -unsigned getTestsDone(); -unsigned getTestsFailed(); +void log(const char* s); +void log_line(const char* s); -void verify(bool success, const char* msg); -void summarizeTests(); +unsigned get_tests_failed_total(); -void log(const char* s); -void logLine(const char* s); +void start_suite(const std::string& name); + +void summarize_suite(); +void summarize_tests(); + +void verify(bool success, const char* msg); -#endif \ No newline at end of file +#endif diff --git a/test/utils.cpp b/test/utils.cpp new file mode 100644 index 0000000..f95d48d --- /dev/null +++ b/test/utils.cpp @@ -0,0 +1,37 @@ +/* +* utils.cpp +* Sean Murthy +* (c) 2020 sigcpp https://sigcpp.github.io. See LICENSE.MD +* +* Attribution and copyright notice must be retained. +* - Attribution may be augmented to include additional authors +* - Copyright notice cannot be altered +* Attribution and copyright info may be relocated but they must be conspicuous. +* +* Define general-purpose utility functions and such +*/ + +#include +#include + +#include "utils.h" + +//replace all instances of a substring with a new substring +void replace_all(std::string& str, const std::string& substr, const std::string& new_substr) +{ + auto pos{ str.find(substr) }; + auto substr_size{ substr.size() }, new_substr_size{ new_substr.size() }; + for (; pos != std::string::npos; pos = str.find(substr, pos + new_substr_size)) + str.replace(pos, substr_size, new_substr); +} + + +//combine a 2-part message to one message +std::string format_message(const std::string_view& base, const std::string& extra) +{ + std::string msg{ base }; + if (!extra.empty()) + msg += ": " + extra; + + return msg; +} diff --git a/test/utils.h b/test/utils.h new file mode 100644 index 0000000..846df98 --- /dev/null +++ b/test/utils.h @@ -0,0 +1,49 @@ +/* +* utils.h +* Sean Murthy +* (c) 2020 sigcpp https://sigcpp.github.io. See LICENSE.MD +* +* Attribution and copyright notice must be retained. +* - Attribution may be augmented to include additional authors +* - Copyright notice cannot be altered +* Attribution and copyright info may be relocated but they must be conspicuous. +* +* Declare general-purpose utility functions and such +*/ + +#ifndef STL_LITE_UTILS_H +#define STL_LITE_UTILS_H + +#include +#include +#include + +void replace_all(std::string& str, const std::string& substr, const std::string& new_substr); + +std::string format_message(const std::string_view& base, const std::string& extra = ""); + +//split a delimited string or string_view into vector +template +std::vector split(const T& s, char delimiter) +{ + static_assert(std::is_same_v || std::is_same_v, + "requires std::string type or std::string_view type"); + + std::vector result; + if (s.empty()) + return result; + + typename T::size_type pos, last_pos{ 0 }; + while ((pos = s.find(delimiter, last_pos)) != T::npos) { + if (s[last_pos] != delimiter) + result.push_back(s.substr(last_pos, pos - last_pos)); + last_pos = pos + 1; + } + + if (last_pos < s.size()) + result.push_back(s.substr(last_pos, pos - last_pos)); + + return result; +} + +#endif \ No newline at end of file diff --git a/test/verifiers.h b/test/verifiers.h new file mode 100644 index 0000000..3222837 --- /dev/null +++ b/test/verifiers.h @@ -0,0 +1,84 @@ +/* +* verifiers.h +* Sean Murthy +* (c) 2020 sigcpp https://sigcpp.github.io. See LICENSE.MD +* +* Attribution and copyright notice must be retained. +* - Attribution may be augmented to include additional authors +* - Copyright notice cannot be altered +* Attribution and copyright info may be relocated but they must be conspicuous. +* +* Declare and define verifiers: verifiers are short-hand functions and call verify internally +* suite runners should call verifiers instead of calling verify function directly +*/ + +#ifndef STL_LITE_VERIFIERS_H +#define STL_LITE_VERIFIERS_H + +#include + +//intentionally declared here instead of including "tester.h" +//suite runners need access only to verifiers and nothing else in the tester +void verify(bool success, const char* msg); + + +inline void is_true(bool value, const char* msg) +{ + verify(value, msg); +} + + +inline void is_false(bool value, const char* msg) +{ + verify(!value, msg); +} + + +//TODO replace this template with a macro to reduce template instantiations +template +constexpr bool is_nonbool_arithmetic() +{ + return std::is_integral_v && !std::is_same_v; +} + + +template +inline void is_zero(T value, const char* msg) +{ + static_assert(is_nonbool_arithmetic(), "requires non-bool arithmetic type"); + verify(value == 0, msg); +} + + +template +inline void is_nonzero(T value, const char* msg) +{ + static_assert(is_nonbool_arithmetic(), "requires non-bool arithmetic type"); + verify(value != 0, msg); +} + + +template +inline void is_negative(T value, const char* msg) +{ + static_assert(is_nonbool_arithmetic(), "requires non-bool arithmetic type"); + verify(value < 0, msg); +} + + +template +inline void is_nonnegative(T value, const char* msg) +{ + static_assert(is_nonbool_arithmetic(), "requires non-bool arithmetic type"); + verify(value >= 0, msg); +} + + +template +inline void is_positive(T value, const char* msg) +{ + static_assert(is_nonbool_arithmetic(), "requires non-bool arithmetic type"); + verify(value > 0, msg); +} + +#endif \ No newline at end of file