diff options
| author | Matthias Melcher <github@matthiasm.com> | 2023-02-23 15:42:05 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-02-23 15:42:05 +0100 |
| commit | 9f87af8ad9a3d8112518b6bbb5b6075881a5b9a2 (patch) | |
| tree | fe96512734abb7b29bc6b8fac9816d7cc0c4ae3f /test | |
| parent | 92818939267fc7fbb6a33d86fb78576f55ce6aca (diff) | |
Fl_String refactoring and extension (#683)
- add true unittest and Fl_String testing
- interface and printout are similar to gtest
without requiring external linkage.
just run `unittest --core`.
- new Fl_String API
- extended API to fl_input_str and fl_password_str
- co-authored-by: Albrecht Schlosser <albrechts.fltk@online.de>
Diffstat (limited to 'test')
| -rw-r--r-- | test/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | test/Makefile | 3 | ||||
| -rw-r--r-- | test/ask.cxx | 9 | ||||
| -rw-r--r-- | test/unittest_core.cxx | 253 | ||||
| -rw-r--r-- | test/unittests.cxx | 319 | ||||
| -rw-r--r-- | test/unittests.h | 174 |
6 files changed, 747 insertions, 12 deletions
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 67c36fb00..b6bc2c303 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -154,6 +154,7 @@ SET (UNITTEST_SRCS unittests.h unittest_about.cxx unittest_points.cxx + unittest_core.cxx unittest_complex_shapes.cxx unittest_fast_shapes.cxx unittest_circles.cxx diff --git a/test/Makefile b/test/Makefile index 193cd9d17..2a5ea0980 100644 --- a/test/Makefile +++ b/test/Makefile @@ -30,7 +30,8 @@ CPPUNITTEST = \ unittest_viewport.cxx \ unittest_scrollbarsize.cxx \ unittest_schemes.cxx \ - unittest_simple_terminal.cxx + unittest_simple_terminal.cxx \ + unittest_core.cxx OBJUNITTEST = \ unittests.o \ diff --git a/test/ask.cxx b/test/ask.cxx index 0c2365149..973a42d11 100644 --- a/test/ask.cxx +++ b/test/ask.cxx @@ -34,16 +34,17 @@ void rename_button(Fl_Widget *o, void *v) { int what = fl_int(v); + int ret = 0; Fl_String input; if (what == 0) { fl_message_icon_label("§"); - input = fl_input_str(0, "Input (no size limit, use ctrl/j for newline):", o->label()); + input = fl_input_str(ret, 0, "Input (no size limit, use ctrl/j for newline):", o->label()); } else { fl_message_icon_label("€"); - input = fl_password_str(20, "Enter password (max. 20 characters):", o->label()); + input = fl_password_str(ret, 20, "Enter password (max. 20 characters):", o->label()); } - if (input.value()) { - o->copy_label(input.value()); + if (ret == 0) { + o->copy_label(input.c_str()); o->redraw(); } } diff --git a/test/unittest_core.cxx b/test/unittest_core.cxx new file mode 100644 index 000000000..7e6699956 --- /dev/null +++ b/test/unittest_core.cxx @@ -0,0 +1,253 @@ +// +// Unit tests for the Fast Light Tool Kit (FLTK). +// +// Copyright 1998-2023 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +#include "unittests.h" + +#include <FL/Fl_Group.H> +#include <FL/Fl_Simple_Terminal.H> +#include <FL/Fl_String.H> + +/* Test Fl_String constructor and assignment. */ +TEST(Fl_String, Assignment) { + Fl_String null; + EXPECT_STREQ(null.c_str(), ""); // default initialisation is an empty string + EXPECT_TRUE(null.empty()); + + Fl_String null2(NULL); + EXPECT_STREQ(null2.c_str(), ""); // initialise with a NULL pointer gets an empty string + EXPECT_TRUE(null2.empty()); + + Fl_String empty(""); + EXPECT_STREQ(empty.c_str(), ""); // also, empty CString make empty Fl_String + EXPECT_TRUE(empty.empty()); + + Fl_String text("hello"); + EXPECT_STREQ(text.c_str(), "hello"); // Load some text from a CString + EXPECT_EQ(text.size(), 5); // did we get the size right? + EXPECT_EQ(text.strlen(), 5); // do we have a trailing 0 + EXPECT_GE(text.capacity(), 5); // do we have the capacity + EXPECT_TRUE(!text.empty()); // test the empty() method + + Fl_String text2("abcdef", 3); + EXPECT_STREQ(text2.c_str(), "abc"); + EXPECT_EQ(text2.size(), 3); + + Fl_String text3("abc\0def", 7); + EXPECT_EQ(text3.strlen(), 3); + EXPECT_EQ(text3.size(), 7); + + Fl_String text4(text); + EXPECT_STREQ(text4.c_str(), "hello"); + + Fl_String text5 = text; + EXPECT_STREQ(text5.c_str(), "hello"); + + Fl_String text6 = "yoohoo"; + EXPECT_STREQ(text6.c_str(), "yoohoo"); + + return true; +} + +/* Test methods that access Fl_String content and parts of it. */ +TEST(Fl_String, Access) { + Fl_String hello = "hello"; + EXPECT_STREQ(hello.c_str(), "hello"); + EXPECT_STREQ(hello.data(), "hello"); + EXPECT_EQ(hello[1], 'e'); + EXPECT_EQ(hello[hello.size()], 0); + EXPECT_EQ(hello.at(1), 'e'); + EXPECT_EQ(hello.at(-1), 0); + EXPECT_EQ(hello.at(11), 0); + + hello[1] = 'a'; + EXPECT_STREQ(hello.c_str(), "hallo"); + + hello.data()[1] = 'e'; + EXPECT_STREQ(hello.c_str(), "hello"); + + return true; +} + +/* Test the Fl_String capacity management. */ +TEST(Fl_String, Capacity) { + Fl_String hello; + EXPECT_EQ(hello.capacity(), 0); + + hello = "hi"; + EXPECT_STREQ(hello.c_str(), "hi"); + EXPECT_GE(hello.capacity(), 2); + + hello = "the quick brown fox jumps over the lazy dog"; + EXPECT_STREQ(hello.c_str(), "the quick brown fox jumps over the lazy dog"); + EXPECT_GE(hello.capacity(), 41); + + int c = hello.capacity(); + hello.reserve(c+100); + EXPECT_STREQ(hello.c_str(), "the quick brown fox jumps over the lazy dog"); + EXPECT_GE(hello.capacity(), 141); + + hello = "hi"; + hello.shrink_to_fit(); + EXPECT_EQ(hello.capacity(), 2); + + return true; +} + +/* Test all methods that operate on Fl_String. */ +TEST(Fl_String, Operations) { + Fl_String empty; + Fl_String hello = "Hello", world = "World"; + hello.resize(4); + EXPECT_STREQ(hello.c_str(), "Hell"); + + hello.clear(); + EXPECT_TRUE(hello.empty()); + + hello = "Hello"; + hello.insert(3, "-"); + EXPECT_STREQ(hello.c_str(), "Hel-lo"); + hello = "Hello"; + hello.erase(2, 2); + EXPECT_STREQ(hello.c_str(), "Heo"); + + hello = "Hello"; + hello.push_back('!'); + EXPECT_STREQ(hello.c_str(), "Hello!"); + hello.pop_back(); + EXPECT_STREQ(hello.c_str(), "Hello"); + hello.append(world); + EXPECT_STREQ(hello.c_str(), "HelloWorld"); + hello.append("!"); + EXPECT_STREQ(hello.c_str(), "HelloWorld!"); + hello = "Hello"; + hello += world; + EXPECT_STREQ(hello.c_str(), "HelloWorld"); + hello += "!"; + EXPECT_STREQ(hello.c_str(), "HelloWorld!"); + hello += '?'; + EXPECT_STREQ(hello.c_str(), "HelloWorld!?"); + + hello = "Hello"; + hello.replace(0, 0, "Say ", 4); + EXPECT_STREQ(hello.c_str(), "Say Hello"); + hello.replace(0, 4, ""); + EXPECT_STREQ(hello.c_str(), "Hello"); + hello.replace(2, 2, "bb"); + EXPECT_STREQ(hello.c_str(), "Hebbo"); + hello.replace(2, 2, "xxx"); + EXPECT_STREQ(hello.c_str(), "Hexxxo"); + hello.replace(2, 3, "ll"); + EXPECT_STREQ(hello.c_str(), "Hello"); + hello.replace(2, 0, NULL, 0); + EXPECT_STREQ(hello.c_str(), "Hello"); + hello.replace(Fl_String::npos, Fl_String::npos, world); + EXPECT_STREQ(hello.c_str(), "HelloWorld"); + + hello = "Hello"; + Fl_String sub = hello.substr(); + EXPECT_STREQ(sub.c_str(), "Hello"); // check correct usage + sub = hello.substr(2); + EXPECT_STREQ(sub.c_str(), "llo"); + sub = hello.substr(2, 2); + EXPECT_STREQ(sub.c_str(), "ll"); + sub = hello.substr(-1, 2); + EXPECT_TRUE(sub.empty()); // check faulty values + sub = hello.substr(20, 2); + EXPECT_TRUE(sub.empty()); + sub = empty.substr(0, 2); + EXPECT_TRUE(sub.empty()); + + return true; +} + +/* Test all Fl_String functions that are no part of the class. */ +TEST(Fl_String, Non-Member Functions) { + Fl_String a = "a", b = "b", empty = "", result; + result = a + b; + EXPECT_STREQ(result.c_str(), "ab"); + result = a + empty; + EXPECT_STREQ(result.c_str(), "a"); + result = a + "c"; + EXPECT_STREQ(result.c_str(), "ac"); + result = empty + "x"; + EXPECT_STREQ(result.c_str(), "x"); + EXPECT_TRUE(!(a == b)); + EXPECT_TRUE(a == a); + EXPECT_TRUE(empty == empty); + EXPECT_TRUE(a+b == "ab"); + EXPECT_TRUE(a+"b" == "a" + b); + + return true; +} + +// +//------- test aspects of the FLTK core library ---------- +// + +/* + Create a tab with only a terminal window in it. When shown for the first time, + unittest will visualize progress by runing all regsitered tests one-by-one + every few miliseconds. + + When run in command line mode (option `--core`), all tests are executed + at full speed. + */ +class Ut_Core_Test : public Fl_Group { + + Fl_Simple_Terminal *tty; + bool suite_ran_; + +public: + + // Create the tab + static Fl_Widget *create() { + return new Ut_Core_Test(UT_TESTAREA_X, UT_TESTAREA_Y, UT_TESTAREA_W, UT_TESTAREA_H); + } + + // Constructor for this tab + Ut_Core_Test(int x, int y, int w, int h) + : Fl_Group(x, y, w, h), + tty(NULL), + suite_ran_(false) + { + tty = new Fl_Simple_Terminal(x+4, y+4, w-8, h-8, "Unittest Log"); + tty->ansi(true); + end(); + Ut_Suite::tty = tty; + } + + // Run one single test and repeat calling this until all tests are done + static void timer_cb(void*) { + // Run a test every few miliseconds to visualize the progress + if (Ut_Suite::run_next_test()) + Fl::repeat_timeout(0.2, timer_cb); + } + + // Showing this tab for the first time will trigger the tests + void show() FL_OVERRIDE { + Fl_Group::show(); + if (!suite_ran_) { + Fl::add_timeout(0.5, timer_cb); + suite_ran_ = true; + } + } +}; + +// Register this tab with the unittest app. +UnitTest core(UT_TEST_CORE, "Core Functionality", Ut_Core_Test::create); + + + diff --git a/test/unittests.cxx b/test/unittests.cxx index e393613fb..b9103feb5 100644 --- a/test/unittests.cxx +++ b/test/unittests.cxx @@ -29,10 +29,12 @@ #include <FL/Fl_Double_Window.H> #include <FL/Fl_Hold_Browser.H> #include <FL/Fl_Help_View.H> +#include <FL/Fl_Simple_Terminal.H> #include <FL/Fl_Group.H> #include <FL/Fl_Box.H> #include <FL/fl_draw.H> // fl_text_extents() #include <FL/fl_string_functions.h> // fl_strdup() +#include <FL/fl_ask.H> // fl_message() #include <stdlib.h> // malloc, free class Ut_Main_Window *mainwin = NULL; @@ -41,6 +43,8 @@ class Fl_Hold_Browser *browser = NULL; int UnitTest::num_tests_ = 0; UnitTest *UnitTest::test_list_[200] = { 0 }; +// ----- UnitTest ------------------------------------------------------ MARK: - + UnitTest::UnitTest(int index, const char* label, Fl_Widget* (*create)()) : widget_(0L) { @@ -77,6 +81,8 @@ void UnitTest::add(int index, UnitTest* t) { num_tests_ = index+1; } +// ----- Ut_Main_Window ------------------------------------------------ MARK: - + Ut_Main_Window::Ut_Main_Window(int w, int h, const char *l) : Fl_Double_Window(w, h, l), draw_alignment_test_(0) @@ -118,10 +124,265 @@ void Ut_Main_Window::test_alignment(int v) { redraw(); } -//------- include the various unit tests as inline code ------- +// ----- Ut_Test ------------------------------------------------------- MARK: - + +/** Create a unit test that can contain many test cases using EXPECT_*. + + Ut_Test should be instantiated by using the TEST(SUITE, NAME) macro. Multiple + TEST() macros can be called within the same source file to generate an + arbitrary number of tests. TEST() can be called in multiple source files + of the same application. + + The constructor also registers the test with Ut_Suite and groups it by name. + */ +Ut_Test::Ut_Test(const char *suitename, const char *testname, Ut_Test_Call call) +: name_(testname), + call_(call), + failed_(false), + done_(false) +{ + Ut_Suite *suite = Ut_Suite::locate(suitename); + suite->add(this); +} + +/** Run all cases inside the test and return false when the first case fails. */ +bool Ut_Test::run(const char *suite) { + Ut_Suite::printf("%s[ RUN ]%s %s.%s\n", + Ut_Suite::green, Ut_Suite::normal, suite, name_); + bool ret = call_(); + if (ret) { + Ut_Suite::printf("%s[ OK ]%s %s.%s\n", + Ut_Suite::green, Ut_Suite::normal, suite, name_); + failed_ = false; + } else { + Ut_Suite::printf("%s[ FAILED ]%s %s.%s\n", + Ut_Suite::red, Ut_Suite::normal, suite, name_); + failed_ = true; + } + done_ = true; + return ret; +} + +/** Print a message is the test was previously marked failed. */ +void Ut_Test::print_failed(const char *suite) { + if (failed_) { + Ut_Suite::printf("%s[ FAILED ]%s %s.%s\n", + Ut_Suite::red, Ut_Suite::normal, suite, name_); + } +} + +// ----- Ut_Suite ------------------------------------------------------ MARK: - + +Ut_Suite **Ut_Suite::suite_list_ = NULL; +int Ut_Suite::suite_list_size_ = 0; +int Ut_Suite::num_tests_ = 0; +int Ut_Suite::num_passed_ = 0; +int Ut_Suite::num_failed_ = 0; +const char *Ut_Suite::red = "\033[31m"; +const char *Ut_Suite::green = "\033[32m"; +const char *Ut_Suite::normal = "\033[0m"; +Fl_Simple_Terminal *Ut_Suite::tty = NULL; + +/** Switch the user of color escape sequnces in the log text. */ +void Ut_Suite::color(int v) { + if (v) { + red = "\033[31m"; + green = "\033[32m"; + normal = "\033[0m"; + } else { + red = ""; + green = ""; + normal = ""; + } +} + +/** Create a suite that will group tests by the suite name. + + Ut_Suite is automatically instantiated by using the TEST(SUITE, NAME) macro. + Multiple TEST() macros are grouped into suits by suite name. + */ +Ut_Suite::Ut_Suite(const char *name) +: test_list_(NULL), + test_list_size_(0), + name_(name), + done_(false) +{ +} + +/** Add a test to the suite. This is done automatically by the TEST() macro. */ +void Ut_Suite::add(Ut_Test *test) { + if ( (test_list_size_ % 16) == 0 ) { + test_list_ = (Ut_Test**)realloc(test_list_, (test_list_size_+16)*sizeof(Ut_Test*)); + } + test_list_[test_list_size_++] = test; +} + +/** Static method that will find or create a suite by name. */ +Ut_Suite *Ut_Suite::locate(const char *name) { + for (int i=0; i<suite_list_size_; i++) { + if (strcmp(name, suite_list_[i]->name_)==0) + return suite_list_[i]; + } + if ( (suite_list_size_ % 16) == 0 ) { + suite_list_ = (Ut_Suite**)realloc(suite_list_, (suite_list_size_+16)*sizeof(Ut_Suite*)); + } + Ut_Suite *s = new Ut_Suite(name); + suite_list_[suite_list_size_++] = s; + return s; +} + +/** Logs the start of a test suite run. */ +void Ut_Suite::print_suite_epilog() { + Ut_Suite::printf("%s[----------]%s %d test%s from %s\n", Ut_Suite::green, Ut_Suite::normal, + test_list_size_, test_list_size_ == 1 ? "" : "s", name_); +} + +/** Run all tests in a single suite, returning the number of failed tests. */ +int Ut_Suite::run() { + print_suite_epilog(); + int num_tests_failed = 0; + for (int i=0; i<test_list_size_; i++) { + if (!test_list_[i]->run(name_)) { + num_tests_failed++; + } + } + return num_tests_failed; +} + +/** Static method to log all tests that are marked failed. */ +void Ut_Suite::print_failed() { + for (int i=0; i<test_list_size_; i++) { + test_list_[i]->print_failed(name_); + } +} + +/** Static method to log the start of a test run. */ +void Ut_Suite::print_prolog() { + int i; + num_tests_ = 0; + num_passed_ = 0; + num_failed_ = 0; + for (i=0; i<suite_list_size_; i++) { + num_tests_ += suite_list_[i]->size(); + } + Ut_Suite::printf("%s[==========]%s Running %d tests from %d test case%s.\n", + Ut_Suite::green, Ut_Suite::normal, + num_tests_, suite_list_size_, + suite_list_size_ == 1 ? "" : "s"); +} + +/** Static method to log the end of a test run. */ +void Ut_Suite::print_epilog() { + int i; + Ut_Suite::printf("%s[==========]%s %d tests from %d test case%s ran.\n", + Ut_Suite::green, Ut_Suite::normal, + num_tests_, suite_list_size_, + suite_list_size_ == 1 ? "" : "s"); + if (num_passed_) { + Ut_Suite::printf("%s[ PASSED ]%s %d test%s.\n", + Ut_Suite::green, Ut_Suite::normal, num_passed_, + num_passed_ == 1 ? "" : "s"); + } + if (num_failed_) { + Ut_Suite::printf("%s[ FAILED ]%s %d test%s, listed below:\n", + Ut_Suite::red, Ut_Suite::normal, num_failed_, + num_failed_ == 1 ? "" : "s"); + } + for (i=0; i<suite_list_size_; i++) { + suite_list_[i]->print_failed(); + } +} + +/** Static method to run all tests in all test suites. + Returns the number of failed tests. + */ +int Ut_Suite::run_all_tests() { + print_prolog(); + // loop through all suites which then loop through all tests + for (int i=0; i<suite_list_size_; i++) { + int n = suite_list_[i]->run(); + num_passed_ += suite_list_[i]->size() - n; + num_failed_ += n; + } + print_epilog(); + return num_failed_; +} + +/** Static method to run all test, one-by-one, until done. + Run all tests by calling `while (Ut_Suite::run_next_test()) { }`. + This is used to visualise test progress with the terminal window by runnig test + asynchronously and adding a noticable delay between calls. + */ +bool Ut_Suite::run_next_test() { + // if all suites are done, print the ending text and return + Ut_Suite *last = suite_list_[suite_list_size_-1]; + if (last->done_) { + print_epilog(); + return false; + } + // if no tests ran yet, print the starting text + Ut_Suite *first = suite_list_[0]; + if (!first->done_ && !first->test_list_[0]->done_) { + print_prolog(); + } + // now find the next test that hasn't ran yet + for (int i=0; i<suite_list_size_; i++) { + Ut_Suite *st = suite_list_[i]; + if (st->done_) continue; + if (!st->test_list_[0]->done_) + st->print_suite_epilog(); + for (int j=0; j<st->test_list_size_; j++) { + if (st->test_list_[j]->done_) continue; + if (st->test_list_[j]->run(st->name_)) num_passed_++; else num_failed_++; + return true; + } + st->done_ = true; + return true; + } + return true; +} + +/** A printf that is redirected to the terminal or stdout. */ +void Ut_Suite::printf(const char *format, ...) +{ + va_list args; + va_start(args, format); + if (tty) { + tty->vprintf(format, args); + } else { + vprintf(format, args); + } + va_end(args); +} + +/** Log the result of a boolean case fail. */ +void Ut_Suite::log_bool(const char *file, int line, const char *cond, bool result, bool expected) { + Ut_Suite::printf("%s(%d): error:\n", file, line); + Ut_Suite::printf("Value of: %s\n", cond); + Ut_Suite::printf("Actual: %s\n", result ? "true" : "false"); + Ut_Suite::printf("Expected: %s\n", expected ? "true" : "false"); +} + +/** Log the result of a string comparison case fail. */ +void Ut_Suite::log_string(const char *file, int line, const char *cond, const char *result, const char *expected) { + Ut_Suite::printf("%s(%d): error:\n", file, line); + Ut_Suite::printf("Value of: %s\n", cond); + Ut_Suite::printf(" Actual: %s\n", result); + Ut_Suite::printf("Expected: %s\n", expected); +} + +/** Log the result of an integer comparison case fail. */ +void Ut_Suite::log_int(const char *file, int line, const char *cond, int result, const char *expected) { + Ut_Suite::printf("%s(%d): error:\n", file, line); + Ut_Suite::printf("Value of: %s\n", cond); + Ut_Suite::printf(" Actual: %d\n", result); + Ut_Suite::printf("Expected: %s\n", expected); +} + +// ----- main ---------------------------------------------------------- MARK: - // callback whenever the browser value changes -void UT_BROWSER_CB(Fl_Widget*, void*) { +void ui_browser_cb(Fl_Widget*, void*) { for ( int t=1; t<=browser->size(); t++ ) { UnitTest* ti = (UnitTest*)browser->data(t); if ( browser->selected(t) ) { @@ -132,11 +393,57 @@ void UT_BROWSER_CB(Fl_Widget*, void*) { } } +static bool run_core_tests_only = false; + +static int arg(int argc, char** argv, int& i) { + if ( strcmp(argv[i], "--core") == 0 ) { + run_core_tests_only = true; + i++; + return 1; + } + if ( strcmp(argv[i], "--color=0") == 0 ) { + Ut_Suite::color(0); + i++; + return 1; + } + if ( strcmp(argv[i], "--color=1") == 0 ) { + Ut_Suite::color(1); + i++; + return 1; + } + if ( (strcmp(argv[i], "--help") == 0) || (strcmp(argv[i], "-h") == 0) ) { + return 0; + } + return 0; +} // This is the main call. It creates the window and adds all previously // registered tests to the browser widget. int main(int argc, char** argv) { - Fl::args(argc, argv); + int i; + if ( Fl::args(argc,argv,i,arg) == 0 ) { // unsupported argument found + static const char *msg = + "usage: %s <switches>\n" + " --core : test core functionality only\n" + " --color=1, --color=0 : print test output in color or plain text" + " --help, -h : print this help page\n"; + const char *app_name = NULL; + if ( (argc > 0) && argv[0] && argv[0][0] ) + app_name = fl_filename_name(argv[0]); + if ( !app_name || !app_name[0]) + app_name = "unittests"; +#ifdef _MSC_VER + fl_message(msg, app_name); +#else + fprintf(stderr, msg, app_name); +#endif + return 1; + } + + if (run_core_tests_only) { + return RUN_ALL_TESTS(); + } + Fl::get_system_colors(); Fl::scheme(Fl::scheme()); // init scheme before instantiating tests Fl::visual(FL_RGB); @@ -146,9 +453,9 @@ int main(int argc, char** argv) { browser = new Fl_Hold_Browser(UT_BROWSER_X, UT_BROWSER_Y, UT_BROWSER_W, UT_BROWSER_H, "Unit Tests"); browser->align(FL_ALIGN_TOP|FL_ALIGN_LEFT); browser->when(FL_WHEN_CHANGED); - browser->callback(UT_BROWSER_CB); + browser->callback(ui_browser_cb); - int i, n = UnitTest::num_tests(); + int n = UnitTest::num_tests(); for (i=0; i<n; i++) { UnitTest* t = UnitTest::test(i); if (t) { @@ -163,6 +470,6 @@ int main(int argc, char** argv) { mainwin->show(argc, argv); // Select first test in browser, and show that test. browser->select(UT_TEST_ABOUT+1); - UT_BROWSER_CB(browser, 0); + ui_browser_cb(browser, 0); return Fl::run(); } diff --git a/test/unittests.h b/test/unittests.h index 8d04f15ca..cee1dfc3f 100644 --- a/test/unittests.h +++ b/test/unittests.h @@ -20,6 +20,10 @@ #include <FL/Fl.H> #include <FL/Fl_Double_Window.H> +#include <stdarg.h> + +class Fl_Simple_Terminal; + // WINDOW/WIDGET SIZES const int UT_MAINWIN_W = 700; // main window w() const int UT_MAINWIN_H = 400; // main window h() @@ -50,7 +54,8 @@ enum { UT_TEST_VIEWPORT, UT_TEST_SCROLLBARSIZE, UT_TEST_SCHEMES, - UT_TEST_SIMPLE_TERMINAL + UT_TEST_SIMPLE_TERMINAL, + UT_TEST_CORE, }; // This class helps to automatically register a new test with the unittest app. @@ -74,6 +79,173 @@ private: static UnitTest* test_list_[]; }; +// The following classes and macros implement a subset of the Google Test API +// without creating any external dependencies. +// +// There is nothing to initialise or set up. Just by including these classes, +// we can create tests anywhere inside the app by simply writing: +// +// TEST(Math, Addition) { +// EXPECT_EQ(3+3, 6); +// return true; +// } +// TEST(Math, Multiplication) { +// EXPECT_EQ(3*3, 9); +// return true; +// } +// RUN_ALL_TESTS(); +// +// The test suite must only be run once. + +typedef bool (*Ut_Test_Call)(); + +/** + Implement a single test which can in turn contain many EXPECT_* macros. + Ut_Test classes are automatically created using the TEST(suite_name, test_name) + macro. Tests with identical suite names are grouped into a single suite. + */ +class Ut_Test { + friend class Ut_Suite; + const char *name_; + Ut_Test_Call call_; + bool failed_; + bool done_; +public: + Ut_Test(const char *suitename, const char *testname, Ut_Test_Call call); + bool run(const char *suite); + void print_failed(const char *suite); +}; + +/** + Implement test registry and the grouping of tests into a suite. This class + holds a number of static elements that register an arbitrary number of tests + and groups them into suites via the TEST() macro. + */ +class Ut_Suite { + static Ut_Suite **suite_list_; + static int suite_list_size_; + static int num_tests_; + static int num_passed_; + static int num_failed_; + + Ut_Test **test_list_; + int test_list_size_; + const char *name_; + bool done_; + Ut_Suite(const char *name); +public: + void add(Ut_Test *test); + int size() { return test_list_size_; } + int run(); + void print_suite_epilog(); + void print_failed(); + static Ut_Suite *locate(const char *name); + static int run_all_tests(); + static bool run_next_test(); + static void printf(const char *format, ...); + static void log_bool(const char *file, int line, const char *cond, bool result, bool expected); + static void log_string(const char *file, int line, const char *cond, const char *result, const char *expected); + static void log_int(const char *file, int line, const char *cond, int result, const char *expected); + static void print_prolog(); + static void print_epilog(); + static void color(int); + static int failed() { return num_failed_; } + static const char *red; + static const char *green; + static const char *normal; + static Fl_Simple_Terminal *tty; +}; + +#define UT_CONCAT_(prefix, suffix) prefix##suffix +#define UT_CONCAT(prefix, suffix) UT_CONCAT_(prefix, suffix) + +/** Create a test function and register it with the test suites. + \param[in] SUITE naming of the test suite for grouping + \param[in] CASE name this test + */ +#define TEST(SUITE, CASE) \ + static bool UT_CONCAT(test_call_, __LINE__)(); \ + Ut_Test UT_CONCAT(test__, __LINE__)(#SUITE, #CASE, UT_CONCAT(test_call_, __LINE__)); \ + static bool UT_CONCAT(test_call_, __LINE__)() + +/** Create a test case where the result is expected to be a boolena with the value true */ +#define EXPECT_TRUE(COND) \ + bool UT_CONCAT(cond, __LINE__) = COND; \ + if (UT_CONCAT(cond, __LINE__) != true) { \ + Ut_Suite::log_bool(__FILE__, __LINE__, #COND, UT_CONCAT(cond, __LINE__), true); \ + return false; \ + } + +/** Create a test case for string comparison. NULL is ok for both arguments. */ +#define EXPECT_STREQ(A, B) \ + const char *UT_CONCAT(a, __LINE__) = A; \ + const char *UT_CONCAT(b, __LINE__) = B; \ + if ( (UT_CONCAT(a, __LINE__)==NULL && UT_CONCAT(b, __LINE__)!=NULL) \ + || (UT_CONCAT(a, __LINE__)!=NULL && UT_CONCAT(b, __LINE__)==NULL) \ + || (UT_CONCAT(b, __LINE__)!=NULL && strcmp(UT_CONCAT(a, __LINE__), UT_CONCAT(b, __LINE__))!=0) ) { \ + Ut_Suite::log_string(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \ + return false; \ + } + +/** Create a test case for integer comparison. */ +#define EXPECT_EQ(A, B) \ + int UT_CONCAT(a, __LINE__) = A; \ + int UT_CONCAT(b, __LINE__) = B; \ + if (UT_CONCAT(a, __LINE__) != UT_CONCAT(b, __LINE__)) { \ + Ut_Suite::log_int(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \ + return false; \ + } + +/** Create a test case for integer comparison. */ +#define EXPECT_NE(A, B) \ + int UT_CONCAT(a, __LINE__) = A; \ + int UT_CONCAT(b, __LINE__) = B; \ + if (UT_CONCAT(a, __LINE__) == UT_CONCAT(b, __LINE__)) { \ + Ut_Suite::log_int(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \ + return false; \ + } + +/** Create a test case for integer comparison. */ +#define EXPECT_LT(A, B) \ + int UT_CONCAT(a, __LINE__) = A; \ + int UT_CONCAT(b, __LINE__) = B; \ + if (UT_CONCAT(a, __LINE__) >= UT_CONCAT(b, __LINE__)) { \ + Ut_Suite::log_int(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \ + return false; \ + } + +/** Create a test case for integer comparison. */ +#define EXPECT_LE(A, B) \ + int UT_CONCAT(a, __LINE__) = A; \ + int UT_CONCAT(b, __LINE__) = B; \ + if (UT_CONCAT(a, __LINE__) > UT_CONCAT(b, __LINE__)) { \ + Ut_Suite::log_int(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \ + return false; \ + } + +/** Create a test case for integer comparison. */ +#define EXPECT_GT(A, B) \ + int UT_CONCAT(a, __LINE__) = A; \ + int UT_CONCAT(b, __LINE__) = B; \ + if (UT_CONCAT(a, __LINE__) <= UT_CONCAT(b, __LINE__)) { \ + Ut_Suite::log_int(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \ + return false; \ + } + +/** Create a test case for integer comparison. */ +#define EXPECT_GE(A, B) \ + int UT_CONCAT(a, __LINE__) = A; \ + int UT_CONCAT(b, __LINE__) = B; \ + if (UT_CONCAT(a, __LINE__) < UT_CONCAT(b, __LINE__)) { \ + Ut_Suite::log_int(__FILE__, __LINE__, #A, UT_CONCAT(a, __LINE__), #B); \ + return false; \ + } + +/** Run all registered suits and their tests, and return the number of failed tests. */ +#define RUN_ALL_TESTS() \ + Ut_Suite::run_all_tests() + + // The main window needs an additional drawing feature in order to support // the viewport alignment test. class Ut_Main_Window : public Fl_Double_Window { |
