diff options
| -rw-r--r-- | .github/workflows/build.yml | 6 | ||||
| -rw-r--r-- | FL/Fl_String.H | 146 | ||||
| -rw-r--r-- | FL/fl_ask.H | 6 | ||||
| -rw-r--r-- | fluid/ExternalCodeEditor_UNIX.cxx | 6 | ||||
| -rw-r--r-- | fluid/ExternalCodeEditor_UNIX.h | 2 | ||||
| -rw-r--r-- | fluid/ExternalCodeEditor_WIN32.cxx | 2 | ||||
| -rw-r--r-- | fluid/Fl_Menu_Type.cxx | 14 | ||||
| -rw-r--r-- | fluid/Fl_Widget_Type.cxx | 12 | ||||
| -rw-r--r-- | fluid/Fl_Window_Type.cxx | 20 | ||||
| -rw-r--r-- | fluid/code.cxx | 22 | ||||
| -rw-r--r-- | fluid/file.cxx | 16 | ||||
| -rw-r--r-- | fluid/fluid.cxx | 32 | ||||
| -rw-r--r-- | fluid/fluid.h | 34 | ||||
| -rw-r--r-- | fluid/widget_panel.fl | 4 | ||||
| -rw-r--r-- | src/Fl_String.cxx | 600 | ||||
| -rw-r--r-- | src/fl_ask.cxx | 72 | ||||
| -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 |
22 files changed, 1481 insertions, 272 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7df43f315..b1265db62 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,12 @@ jobs: # Execute the build. You can specify a specific target with "--target <NAME>" run: cmake --build . --config $BUILD_TYPE + - name: Unittest + working-directory: ${{github.workspace}}/build + shell: bash + run: ./bin/test/unittests --core + + build-wayland: runs-on: ubuntu-latest diff --git a/FL/Fl_String.H b/FL/Fl_String.H index 72e90f52d..bb38776ad 100644 --- a/FL/Fl_String.H +++ b/FL/Fl_String.H @@ -21,94 +21,110 @@ Basic Fl_String class for FLTK. */ +// See: https://en.cppreference.com/w/cpp/string/basic_string/basic_string + /** Fl_String is the basic string class for FLTK. In this version Fl_String can be used to store strings, copy strings, - and move strings. There are no string manipulation methods yet. + move strings, and do basic string manipulation. Fl_String implements a + subset of std::string with a couple of extensions. std::string should be + a drop-in if we ever decide to allow templates and the std library. - Fl_String can hold the value of an Fl_Input widget including \e nul bytes - if the constructor Fl_String(const char *str, int size) is used. + Fl_String always maintains a trailing \e nul byte, but can also contain + \e nul bytes insde the string if the constructor + Fl_String(const char *str, int size) is used. Assignment and copy constructors \b copy the string value such that the source string can be freed immediately after the assignment. - The string value() can be an empty string \c "" or \c NULL. - - If value() is not \c NULL it is guaranteed that the string is terminated by - a trailing \c nul byte even if the string contains embedded \c nul bytes. + c_str() and data() can be an empty string \c "", but never be \c NULL. The method size() returns the full string size, whether the string contains - embedded \c nul bytes or not. The special method slen() returns 0 if value() - is \c NULL, otherwise the same as \c strlen() would do. - - Examples: - \code - Fl_String np(NULL); - printf(" np : value = %p, size = %d, slen = %d\n", np.value(), np.size(), np.slen()); - Fl_String empty(""); - printf(" empty : value = %p, size = %d\n", empty.value(), empty.size()); - Fl_String fltk("FLTK"); - Fl_Input i(0, 0, 0, 0); - i.value("abc\0def", 7); - Fl_String str(i.value(), i.size()); - printf(" str : strlen = %lu, size = %d, capacity = %d\n", - strlen(str.value()), str.size(), str.capacity()); - - Output: - - np : value = (nil), size = 0, slen = 0 - empty : value = 0x562840befbf0, size = 0 - str : strlen = 3, size = 7, capacity = 15 - \endcode + embedded \c nul bytes or not. The special method Fl_String::strlen() returns + the length of the string up to the first \e nul. + + All methods of Fl_String work on a byte level. They are not UTF-8 aware, + but may hold and manipulate UTF-8 strings if done with care. \since 1.4.0 */ - class Fl_String { + private: + /* + FLTK does no small string optimisation. + If the string is empty and capacity is not set, buffer_ will be NULL. + */ + char *buffer_; int size_; - char *value_; int capacity_; -public: - Fl_String(); - Fl_String(const char *str); - Fl_String(const char *str, int size); - - // copy constructor - Fl_String(const Fl_String &in); - - // copy assignment operator - Fl_String& operator=(const Fl_String &in); + void init_(); + void grow_(int n); + void shrink_(int n); + Fl_String &replace_(int at, int n_del, const char *src, int n_ins); - // assignment operator for 'const char *' - Fl_String& operator=(const char *in); - - virtual ~Fl_String(); - -private: - void init(); - void alloc_buf(int size, bool preserve_text=false); - void release(); +protected: + static const char NUL; public: - void value(const char *str); - void value(const char *str, int len); - /** Returns a pointer to the first character stored in the object */ - const char *value() const { return value_; } - /** Returns a pointer to the first byte stored in the object */ - char *buffer() { return value_; } - /** Returns the number of bytes currently stored in the object */ - int size() const { return size_; } - - int slen() const; - int capacity() const; - void capacity(int num_bytes); - - void debug(const char *info = 0) const; // output string info - void hexdump(const char *info = 0) const; // output string info + hexdump + static const int npos; + // ---- Assignment + Fl_String(); + Fl_String(const Fl_String &str); + Fl_String(const char *cstr); + Fl_String(const char *str, int size); + ~Fl_String(); + Fl_String& operator=(const Fl_String &str); + Fl_String& operator=(const char *cstr); + Fl_String &assign(const Fl_String &str); + Fl_String &assign(const char *cstr); + Fl_String &assign(const char *str, int size); + + // ---- Element Access + char at(int pos) const; + char operator[](int n) const; + char &operator[](int n); + const char *data() const; + char *data(); + const char *c_str() const; + + // ---- Capacity + bool empty() const; + int size() const; + void reserve(int n); + int capacity() const; + void shrink_to_fit(); + + // --- Operations + void clear(); + Fl_String &insert(int at, const char *src, int n_ins=npos); + Fl_String &insert(int at, const Fl_String &src); + Fl_String &erase(int at, int n_del); + void push_back(char c); + void pop_back(); + Fl_String &append(const char *src, int n_ins=npos); + Fl_String &append(const Fl_String &src); + Fl_String &append(char c); + Fl_String &operator+=(const char *src); + Fl_String &operator+=(const Fl_String &src); + Fl_String &operator+=(char c); + Fl_String &replace(int at, int n_del, const char *src, int n_ins=npos); + Fl_String &replace(int at, int n_del, const Fl_String &src); + Fl_String substr(int pos=0, int n=npos) const; + void resize(int n); + + // --- Non Standard + int strlen() const; + void debug(const char *info = 0) const; + void hexdump(const char *info = 0) const; }; // class Fl_String +// ---- Non-member functions +extern Fl_String operator+(const Fl_String &lhs, const Fl_String &rhs); +extern Fl_String operator+(const Fl_String &lhs, const char *rhs); +extern bool operator==(const Fl_String &lhs, const Fl_String &rhs); + #endif // _FL_Fl_String_H_ diff --git a/FL/fl_ask.H b/FL/fl_ask.H index a315fb1f2..c4eb3ab86 100644 --- a/FL/fl_ask.H +++ b/FL/fl_ask.H @@ -72,9 +72,15 @@ FL_EXPORT int fl_choice_n(const char *q, const char *b0, const char *b1, const c FL_EXPORT Fl_String fl_input_str(int maxchar, const char *label, const char *deflt = 0, ...) __fl_attr((__format__(__printf__, 2, 4))); +FL_EXPORT Fl_String fl_input_str(int &ret, int maxchar, const char *label, const char *deflt = 0, ...) + __fl_attr((__format__(__printf__, 3, 5))); + FL_EXPORT Fl_String fl_password_str(int maxchar, const char *label, const char *deflt = 0, ...) __fl_attr((__format__(__printf__, 2, 4))); +FL_EXPORT Fl_String fl_password_str(int &ret, int maxchar, const char *label, const char *deflt = 0, ...) + __fl_attr((__format__(__printf__, 3, 5))); + FL_EXPORT Fl_Widget *fl_message_icon(); extern FL_EXPORT Fl_Font fl_message_font_; extern FL_EXPORT Fl_Fontsize fl_message_size_; diff --git a/fluid/ExternalCodeEditor_UNIX.cxx b/fluid/ExternalCodeEditor_UNIX.cxx index e1e2c0e60..a669d8188 100644 --- a/fluid/ExternalCodeEditor_UNIX.cxx +++ b/fluid/ExternalCodeEditor_UNIX.cxx @@ -299,7 +299,7 @@ const char* ExternalCodeEditor::tmp_filename() { static char path[FL_PATH_MAX+1]; const char *tmpdir = create_tmpdir(); if ( !tmpdir ) return 0; - const char *ext = g_project.code_file_name; // e.g. ".cxx" + const char *ext = g_project.code_file_name.c_str(); // e.g. ".cxx" snprintf(path, FL_PATH_MAX, "%s/%p%s", tmpdir, (void*)this, ext); path[FL_PATH_MAX] = 0; return path; @@ -385,7 +385,7 @@ int ExternalCodeEditor::start_editor(const char *editor_cmd, editor_cmd, filename); char cmd[1024]; snprintf(cmd, sizeof(cmd), "%s %s", editor_cmd, filename); - command_line_.value(editor_cmd); + command_line_ = editor_cmd; open_alert_pipe(); // Fork editor to background.. switch ( pid_ = fork() ) { @@ -559,7 +559,7 @@ void ExternalCodeEditor::alert_pipe_cb(FL_SOCKET s, void* d) { self->last_error_ = 0; if (::read(s, &self->last_error_, sizeof(int)) != sizeof(int)) return; - const char* cmd = self->command_line_.value(); + const char* cmd = self->command_line_.c_str(); if (cmd && *cmd) { if (cmd[0] == '/') { // is this an absoluet filename? fl_alert("Can't launch external editor '%s':\n%s\n\ncmd: \"%s\"", diff --git a/fluid/ExternalCodeEditor_UNIX.h b/fluid/ExternalCodeEditor_UNIX.h index fd3a93b7b..a619cd352 100644 --- a/fluid/ExternalCodeEditor_UNIX.h +++ b/fluid/ExternalCodeEditor_UNIX.h @@ -24,7 +24,7 @@ class ExternalCodeEditor { time_t file_mtime_; // last modify time of the file (used to determine if file changed) size_t file_size_; // last file size (used to determine if changed) const char *filename_; - Fd_String command_line_; + Fl_String command_line_; int last_error_; int alert_pipe_[2]; bool alert_pipe_open_; diff --git a/fluid/ExternalCodeEditor_WIN32.cxx b/fluid/ExternalCodeEditor_WIN32.cxx index bd334e5c0..1afb0afff 100644 --- a/fluid/ExternalCodeEditor_WIN32.cxx +++ b/fluid/ExternalCodeEditor_WIN32.cxx @@ -389,7 +389,7 @@ const char* ExternalCodeEditor::tmp_filename() { static char path[512]; const char *tmpdir = create_tmpdir(); if ( !tmpdir ) return 0; - const char *ext = g_project.code_file_name; // e.g. ".cxx" + const char *ext = g_project.code_file_name.c_str(); // e.g. ".cxx" _snprintf(path, sizeof(path), "%s\\%p%s", tmpdir, (void*)this, ext); path[sizeof(path)-1] = 0; return path; diff --git a/fluid/Fl_Menu_Type.cxx b/fluid/Fl_Menu_Type.cxx index 9d88f56c8..2f89c9572 100644 --- a/fluid/Fl_Menu_Type.cxx +++ b/fluid/Fl_Menu_Type.cxx @@ -381,7 +381,7 @@ void Fl_Menu_Item_Type::write_item(Fd_Code_Writer& f) { switch (g_project.i18n_type) { case 1: // we will call i18n when the menu is instantiated for the first time - f.write_c("%s(", g_project.i18n_static_function.value()); + f.write_c("%s(", g_project.i18n_static_function.c_str()); f.write_cstring(label()); f.write_c(")"); break; @@ -482,11 +482,11 @@ void Fl_Menu_Item_Type::write_code1(Fd_Code_Writer& f) { f.write_c("%sml->labelb = o->label();\n", f.indent()); } else if (g_project.i18n_type==1) { f.write_c("%sml->labelb = %s(o->label());\n", - f.indent(), g_project.i18n_function.value()); + f.indent(), g_project.i18n_function.c_str()); } else if (g_project.i18n_type==2) { f.write_c("%sml->labelb = catgets(%s,%s,i+%d,o->label());\n", - f.indent(), g_project.i18n_file[0] ? g_project.i18n_file.value() : "_catalog", - g_project.i18n_set.value(), msgnum()); + f.indent(), g_project.i18n_file[0] ? g_project.i18n_file.c_str() : "_catalog", + g_project.i18n_set.c_str(), msgnum()); } f.write_c("%sml->typea = FL_IMAGE_LABEL;\n", f.indent()); f.write_c("%sml->typeb = FL_NORMAL_LABEL;\n", f.indent()); @@ -504,11 +504,11 @@ void Fl_Menu_Item_Type::write_code1(Fd_Code_Writer& f) { start_menu_initialiser(f, menuItemInitialized, mname, i); if (g_project.i18n_type==1) { f.write_c("%so->label(%s(o->label()));\n", - f.indent(), g_project.i18n_function.value()); + f.indent(), g_project.i18n_function.c_str()); } else if (g_project.i18n_type==2) { f.write_c("%so->label(catgets(%s,%s,i+%d,o->label()));\n", - f.indent(), g_project.i18n_file[0] ? g_project.i18n_file.value() : "_catalog", - g_project.i18n_set.value(), msgnum()); + f.indent(), g_project.i18n_file[0] ? g_project.i18n_file.c_str() : "_catalog", + g_project.i18n_set.c_str(), msgnum()); } } } diff --git a/fluid/Fl_Widget_Type.cxx b/fluid/Fl_Widget_Type.cxx index afdb61823..3fb265975 100644 --- a/fluid/Fl_Widget_Type.cxx +++ b/fluid/Fl_Widget_Type.cxx @@ -2917,13 +2917,13 @@ void Fl_Widget_Type::write_code1(Fd_Code_Writer& f) { f.write_cstring(label()); break; case 1 : /* GNU gettext */ - f.write_c("%s(", g_project.i18n_function.value()); + f.write_c("%s(", g_project.i18n_function.c_str()); f.write_cstring(label()); f.write_c(")"); break; case 2 : /* POSIX catgets */ - f.write_c("catgets(%s,%s,%d,", g_project.i18n_file[0] ? g_project.i18n_file.value() : "_catalog", - g_project.i18n_set.value(), msgnum()); + f.write_c("catgets(%s,%s,%d,", g_project.i18n_file[0] ? g_project.i18n_file.c_str() : "_catalog", + g_project.i18n_set.c_str(), msgnum()); f.write_cstring(label()); f.write_c(")"); break; @@ -2990,13 +2990,13 @@ void Fl_Widget_Type::write_widget_code(Fd_Code_Writer& f) { f.write_cstring(tooltip()); break; case 1 : /* GNU gettext */ - f.write_c("%s(", g_project.i18n_function.value()); + f.write_c("%s(", g_project.i18n_function.c_str()); f.write_cstring(tooltip()); f.write_c(")"); break; case 2 : /* POSIX catgets */ - f.write_c("catgets(%s,%s,%d,", g_project.i18n_file[0] ? g_project.i18n_file.value() : "_catalog", - g_project.i18n_set.value(), msgnum() + 1); + f.write_c("catgets(%s,%s,%d,", g_project.i18n_file[0] ? g_project.i18n_file.c_str() : "_catalog", + g_project.i18n_set.c_str(), msgnum() + 1); f.write_cstring(tooltip()); f.write_c(")"); break; diff --git a/fluid/Fl_Window_Type.cxx b/fluid/Fl_Window_Type.cxx index 90a573288..78c66cbde 100644 --- a/fluid/Fl_Window_Type.cxx +++ b/fluid/Fl_Window_Type.cxx @@ -196,15 +196,15 @@ void show_project_cb(Fl_Widget *, void *) { use_FL_COMMAND_button->value(g_project.use_FL_COMMAND); utf8_in_src_button->value(g_project.utf8_in_src); avoid_early_includes_button->value(g_project.avoid_early_includes); - header_file_input->value(g_project.header_file_name); - code_file_input->value(g_project.code_file_name); + header_file_input->value(g_project.header_file_name.c_str()); + code_file_input->value(g_project.code_file_name.c_str()); i18n_type_chooser->value(g_project.i18n_type); - i18n_function_input->value(g_project.i18n_function); - i18n_static_function_input->value(g_project.i18n_static_function); - i18n_file_input->value(g_project.i18n_file); - i18n_set_input->value(g_project.i18n_set); - i18n_include_input->value(g_project.i18n_include); - i18n_conditional_input->value(g_project.i18n_conditional); + i18n_function_input->value(g_project.i18n_function.c_str()); + i18n_static_function_input->value(g_project.i18n_static_function.c_str()); + i18n_file_input->value(g_project.i18n_file.c_str()); + i18n_set_input->value(g_project.i18n_set.c_str()); + i18n_include_input->value(g_project.i18n_include.c_str()); + i18n_conditional_input->value(g_project.i18n_conditional.c_str()); switch (g_project.i18n_type) { case 0 : /* None */ i18n_include_input->hide(); @@ -258,12 +258,12 @@ void show_settings_cb(Fl_Widget *, void *) { } void header_input_cb(Fl_Input* i, void*) { - if (strcmp(g_project.header_file_name, i->value())) + if (strcmp(g_project.header_file_name.c_str(), i->value())) set_modflag(1); g_project.header_file_name = i->value(); } void code_input_cb(Fl_Input* i, void*) { - if (strcmp(g_project.code_file_name, i->value())) + if (strcmp(g_project.code_file_name.c_str(), i->value())) set_modflag(1); g_project.code_file_name = i->value(); } diff --git a/fluid/code.cxx b/fluid/code.cxx index 378cb1303..f9f7a62df 100644 --- a/fluid/code.cxx +++ b/fluid/code.cxx @@ -134,7 +134,7 @@ int write_strings(const char *sfile) { case 2 : /* POSIX catgets, put a .msg file out */ fprintf(fp, "$ generated by Fast Light User Interface Designer (fluid) version %.4f\n", FL_VERSION); - fprintf(fp, "$set %s\n", g_project.i18n_set.value()); + fprintf(fp, "$set %s\n", g_project.i18n_set.c_str()); fputs("$quote \"\n", fp); for (i = 1, p = Fl_Type::first; p; p = p->next) { @@ -768,7 +768,7 @@ int Fd_Code_Writer::write_code(const char *s, const char *t, bool to_sourceview) if (t && g_project.include_H_from_C) { if (to_sourceview) { write_c("#include \"CodeView.h\"\n"); - } else if (g_project.header_file_name[0] == '.' && strchr(g_project.header_file_name, '/') == NULL) { + } else if (g_project.header_file_name[0] == '.' && strchr(g_project.header_file_name.c_str(), '/') == NULL) { write_c("#include \"%s\"\n", fl_filename_name(t)); } else { write_c("#include \"%s\"\n", t); @@ -777,30 +777,30 @@ int Fd_Code_Writer::write_code(const char *s, const char *t, bool to_sourceview) if (g_project.i18n_type && g_project.i18n_include[0]) { int conditional = (g_project.i18n_conditional[0]!=0); if (conditional) { - write_c("#ifdef %s\n", g_project.i18n_conditional.value()); + write_c("#ifdef %s\n", g_project.i18n_conditional.c_str()); indentation++; } if (g_project.i18n_include[0] != '<' && g_project.i18n_include[0] != '\"') - write_c("#%sinclude \"%s\"\n", indent(), g_project.i18n_include.value()); + write_c("#%sinclude \"%s\"\n", indent(), g_project.i18n_include.c_str()); else - write_c("#%sinclude %s\n", indent(), g_project.i18n_include.value()); + write_c("#%sinclude %s\n", indent(), g_project.i18n_include.c_str()); if (g_project.i18n_type == 2) { if (g_project.i18n_file[0]) { - write_c("extern nl_catd %s;\n", g_project.i18n_file.value()); + write_c("extern nl_catd %s;\n", g_project.i18n_file.c_str()); } else { write_c("// Initialize I18N stuff now for menus...\n"); write_c("#%sinclude <locale.h>\n", indent()); write_c("static char *_locale = setlocale(LC_MESSAGES, \"\");\n"); - write_c("static nl_catd _catalog = catopen(\"%s\", 0);\n", g_project.i18n_program.value()); + write_c("static nl_catd _catalog = catopen(\"%s\", 0);\n", g_project.i18n_program.c_str()); } } if (conditional) { write_c("#else\n"); if (g_project.i18n_type == 1) { if (g_project.i18n_function[0]) { - write_c("#%sifndef %s\n", indent(), g_project.i18n_function.value()); - write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_function.value()); + write_c("#%sifndef %s\n", indent(), g_project.i18n_function.c_str()); + write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_function.c_str()); write_c("#%sendif\n", indent()); } } @@ -813,8 +813,8 @@ int Fd_Code_Writer::write_code(const char *s, const char *t, bool to_sourceview) write_c("#endif\n"); } if (g_project.i18n_type == 1 && g_project.i18n_static_function[0]) { - write_c("#ifndef %s\n", g_project.i18n_static_function.value()); - write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_static_function.value()); + write_c("#ifndef %s\n", g_project.i18n_static_function.c_str()); + write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_static_function.c_str()); write_c("#endif\n"); } } diff --git a/fluid/file.cxx b/fluid/file.cxx index 3d2d8745f..db59a2733 100644 --- a/fluid/file.cxx +++ b/fluid/file.cxx @@ -771,26 +771,26 @@ int Fd_Project_Writer::write_project(const char *filename, int selected_only) { write_string("\navoid_early_includes"); if (g_project.i18n_type) { write_string("\ni18n_type %d", g_project.i18n_type); - write_string("\ni18n_include"); write_word(g_project.i18n_include); - write_string("\ni18n_conditional"); write_word(g_project.i18n_conditional); + write_string("\ni18n_include"); write_word(g_project.i18n_include.c_str()); + write_string("\ni18n_conditional"); write_word(g_project.i18n_conditional.c_str()); switch (g_project.i18n_type) { case 1 : /* GNU gettext */ - write_string("\ni18n_function"); write_word(g_project.i18n_function); - write_string("\ni18n_static_function"); write_word(g_project.i18n_static_function); + write_string("\ni18n_function"); write_word(g_project.i18n_function.c_str()); + write_string("\ni18n_static_function"); write_word(g_project.i18n_static_function.c_str()); break; case 2 : /* POSIX catgets */ if (g_project.i18n_file[0]) { write_string("\ni18n_file"); - write_word(g_project.i18n_file); + write_word(g_project.i18n_file.c_str()); } - write_string("\ni18n_set"); write_word(g_project.i18n_set); + write_string("\ni18n_set"); write_word(g_project.i18n_set.c_str()); break; } } if (!selected_only) { - write_string("\nheader_name"); write_word(g_project.header_file_name); - write_string("\ncode_name"); write_word(g_project.code_file_name); + write_string("\nheader_name"); write_word(g_project.header_file_name.c_str()); + write_string("\ncode_name"); write_word(g_project.code_file_name.c_str()); #if 0 // https://github.com/fltk/fltk/issues/328 diff --git a/fluid/fluid.cxx b/fluid/fluid.cxx index 56fc38b4d..7512e7bc8 100644 --- a/fluid/fluid.cxx +++ b/fluid/fluid.cxx @@ -164,8 +164,8 @@ int compile_strings = 0; // fluic -cs int batch_mode = 0; // if set (-c, -u) don't open display /// command line arguments override settings in the projectfile -Fd_String g_code_filename_arg; -Fd_String g_header_filename_arg; +Fl_String g_code_filename_arg; +Fl_String g_header_filename_arg; /** \var int Fluid_Project::header_file_set If set, commandline overrides header file name in .fl file. @@ -1001,19 +1001,20 @@ int write_code_files() { char cname[FL_PATH_MAX+1]; char hname[FL_PATH_MAX+1]; g_project.i18n_program = fl_filename_name(filename); - g_project.i18n_program.capacity(FL_PATH_MAX); - fl_filename_setext(g_project.i18n_program.buffer(), FL_PATH_MAX, ""); - if (g_project.code_file_name[0] == '.' && strchr(g_project.code_file_name, '/') == NULL) { + g_project.i18n_program.resize(FL_PATH_MAX); + fl_filename_setext(g_project.i18n_program.data(), FL_PATH_MAX, ""); + g_project.i18n_program.resize(g_project.i18n_program.strlen()); + if (g_project.code_file_name[0] == '.' && strchr(g_project.code_file_name.c_str(), '/') == NULL) { strlcpy(cname, fl_filename_name(filename), FL_PATH_MAX); - fl_filename_setext(cname, FL_PATH_MAX, g_project.code_file_name); + fl_filename_setext(cname, FL_PATH_MAX, g_project.code_file_name.c_str()); } else { - strlcpy(cname, g_project.code_file_name, FL_PATH_MAX); + strlcpy(cname, g_project.code_file_name.c_str(), FL_PATH_MAX); } - if (g_project.header_file_name[0] == '.' && strchr(g_project.header_file_name, '/') == NULL) { + if (g_project.header_file_name[0] == '.' && strchr(g_project.header_file_name.c_str(), '/') == NULL) { strlcpy(hname, fl_filename_name(filename), FL_PATH_MAX); - fl_filename_setext(hname, FL_PATH_MAX, g_project.header_file_name); + fl_filename_setext(hname, FL_PATH_MAX, g_project.header_file_name.c_str()); } else { - strlcpy(hname, g_project.header_file_name, FL_PATH_MAX); + strlcpy(hname, g_project.header_file_name.c_str(), FL_PATH_MAX); } if (!batch_mode) enter_project_dir(); Fd_Code_Writer f; @@ -1805,7 +1806,7 @@ void set_modflag(int mf, int mfc) { #endif // _WIN32 else basename = filename; - code_ext = fl_filename_ext(g_project.code_file_name); + code_ext = fl_filename_ext(g_project.code_file_name.c_str()); char mod_star = modflag ? '*' : ' '; char mod_c_star = modflag_c ? '*' : ' '; snprintf(title, sizeof(title), "%s%c %s%c", @@ -1927,11 +1928,12 @@ void update_sourceview_cb(Fl_Button*, void*) sv_strings->scroll(top, 0); } else if (sv_source->visible_r() || sv_header->visible_r()) { g_project.i18n_program = fl_filename_name(sv_source_filename); - g_project.i18n_program.capacity(FL_PATH_MAX); - fl_filename_setext(g_project.i18n_program.buffer(), FL_PATH_MAX, ""); - Fd_String code_file_name_bak = g_project.code_file_name; + g_project.i18n_program.resize(FL_PATH_MAX); + fl_filename_setext(g_project.i18n_program.data(), FL_PATH_MAX, ""); + g_project.i18n_program.resize(g_project.i18n_program.strlen()); + Fl_String code_file_name_bak = g_project.code_file_name; g_project.code_file_name = sv_source_filename; - Fd_String header_file_name_bak = g_project.header_file_name; + Fl_String header_file_name_bak = g_project.header_file_name; g_project.header_file_name = sv_header_filename; // generate the code and load the files diff --git a/fluid/fluid.h b/fluid/fluid.h index c7b800849..7d2a3ade2 100644 --- a/fluid/fluid.h +++ b/fluid/fluid.h @@ -78,18 +78,6 @@ extern int batch_mode; extern int pasteoffset; -// ---- string handling - -class Fd_String : public Fl_String -{ -public: - Fd_String() : Fl_String("") { } - Fd_String(const char* s) : Fl_String(s) { } - int empty() { return size()==0; } - void operator=(const char* s) { value(s); } - operator const char* () const { return value(); } -}; - // ---- project settings class Fluid_Project { @@ -99,27 +87,27 @@ public: void reset(); int i18n_type; - Fd_String i18n_include; - Fd_String i18n_conditional; - Fd_String i18n_function; - Fd_String i18n_static_function; - Fd_String i18n_file; - Fd_String i18n_set; - Fd_String i18n_program; + Fl_String i18n_include; + Fl_String i18n_conditional; + Fl_String i18n_function; + Fl_String i18n_static_function; + Fl_String i18n_file; + Fl_String i18n_set; + Fl_String i18n_program; int include_H_from_C; int use_FL_COMMAND; int utf8_in_src; int avoid_early_includes; int header_file_set; int code_file_set; - Fd_String header_file_name; - Fd_String code_file_name; + Fl_String header_file_name; + Fl_String code_file_name; }; extern Fluid_Project g_project; -extern Fd_String g_code_filename_arg; -extern Fd_String g_header_filename_arg; +extern Fl_String g_code_filename_arg; +extern Fl_String g_header_filename_arg; // ---- public functions diff --git a/fluid/widget_panel.fl b/fluid/widget_panel.fl index afd6bb62a..15f28e24b 100644 --- a/fluid/widget_panel.fl +++ b/fluid/widget_panel.fl @@ -66,7 +66,7 @@ Use Ctrl-J for newlines.} xywh {95 40 190 20} labelfont 1 labelsize 11 when 15 t xywh {95 65 309 20} labelfont 1 labelsize 11 align 4 } { Fl_Input {} { - callback image_cb + callback image_cb selected tooltip {The active image for the widget.} xywh {95 65 200 20} labelfont 1 labelsize 11 textsize 11 resizable } Fl_Button {} { @@ -471,7 +471,7 @@ h, ph, sh, ch, and i} xywh {275 150 55 20} labelsize 11 align 5 textsize 11 } {} Fl_Button {} { callback shortcut_in_cb - comment {This is a special button that grabs keystrokes directly} selected + comment {This is a special button that grabs keystrokes directly} tooltip {The shortcut key for the widget. Use 'Backspace' key to clear.} xywh {95 210 310 20} box DOWN_BOX color 7 selection_color 12 labelsize 11 when 1 code0 {\#include <FL/Fl_Shortcut_Button.H>} diff --git a/src/Fl_String.cxx b/src/Fl_String.cxx index e07f23a94..222e34583 100644 --- a/src/Fl_String.cxx +++ b/src/Fl_String.cxx @@ -17,137 +17,520 @@ #include <FL/Fl_String.H> #include <stdio.h> +#include <stdlib.h> #include <string.h> +#include <limits.h> + /** \file src/Fl_String.cxx - Basic Fl_String class for FLTK. -*/ + Basic Fl_String class for FLTK. + */ + + +/* + If buffer_ is NULL, c_str() and buffer() will point here. + */ +const char Fl_String::NUL = 0; + +/** + Indicate a maximum value or error. + This value is generally used as end of string indicator or as the error + indicator by the functions that return a string index. + */ +const int Fl_String::npos = INT_MAX; + +/** + Initialise the class instance. + */ +void Fl_String::init_() { + buffer_ = NULL; + size_ = 0; + capacity_ = 0; +} + +/** + Grow the buffer to a capacity of at least n bytes. + + This method will always grow the buffer size, or keep it as is, but never + shrink it. Use shrink_to_fit() to shrink the buffer as much as possible. + + \param[in] n number of bytes needed, not counting the trailing NUL + */ +void Fl_String::grow_(int n) { + if (n <= capacity_) + return; + int alloc_size_ = n + 1; // trailing NUL + // round n up so we can grow in chunks + if (alloc_size_ <= 24) { // allocate at least 24 bytes + alloc_size_ = 24; + } else if (alloc_size_ < 1024) { + alloc_size_ = (alloc_size_+128) & ~127; // allocate in 128 byte chunks + } else { + alloc_size_ = (alloc_size_+2048) & ~2047; // allocate in 2k chunks + } + // allocate now + char *new_buffer = (char*)::malloc(alloc_size_); + if (buffer_ && (size_ > 0)) { + memcpy(new_buffer, buffer_, size_); + ::free(buffer_); + } + if (size_ >= 0) + new_buffer[size_] = 0; // trailing NUL + buffer_ = new_buffer; + capacity_ = alloc_size_-1; // trailing NUL +} -/** Constructs an empty string */ +/** + Shrink the buffer to n bytes, or size, if size > n. + + Shrink the buffer as much as possible. If \p n is 0 and the string is empty, + the buffer will be released. + + \param[in] n shrink buffer to n bytes, not counting the trailing NUL + */ +void Fl_String::shrink_(int n) { + if (n < size_) + n = size_; + if (n == capacity_) + return; + if (n == 0) { + if (buffer_) + ::free(buffer_); + buffer_ = NULL; + } else { + buffer_ = (char*)::realloc(buffer_, n+1); // NUL + buffer_[size_] = 0; // trailing NUL + } + capacity_ = n; +} + +/** + Remove \p n_del bytes at \p at and insert \p n_ins bytes from \p src. + + String will remain NUL terminated. Data in \p ins may contain NULs. + + \param[in] at remove and insert bytes at this index + \param[in] n_del number of bytes to remove + \param[in] ins insert bytes from here, can be NULL if \p n_ins is also 0 + \param[in] n_ins number of bytes to insert + \return self + */ +Fl_String &Fl_String::replace_(int at, int n_del, const char *ins, int n_ins) { + if (at > size_) at = size_; + if (n_del > size_ - at) n_del = size_ - at; + int diff = n_ins - n_del, new_size = size_ + diff; + if (diff) { + int src = at + n_del, dst = at + n_ins, n = size_ - src; + grow_(new_size); + if (n > 0) ::memmove(buffer_+dst, buffer_+src, n); + } + if (n_ins > 0) { + ::memmove(buffer_+at, ins, n_ins); + } + size_ = new_size; + buffer_[size_] = 0; + return *this; +} + + +// ---- Assignment ----------------------------------------------------- MARK: - + +/** + Allocate an empty string. + */ Fl_String::Fl_String() { - init(); + init_(); +} + +/** + Copy constructor. + \param[in] str copy from another Fl_String + */ +Fl_String::Fl_String(const Fl_String &str) { + init_(); + assign(str); } -/** Constructor from a C-style string */ -Fl_String::Fl_String(const char *str) { - init(); - value(str); +/** + Constructor from a C-style string. + \param[in] cstr a NUL terminated C-style string + */ +Fl_String::Fl_String(const char *cstr) { + init_(); + assign(cstr); } -/** Constructor from a buffer of \c size bytes */ +/** + Constructor from data of \p size bytes. + \param[in] str a block of data that may contain NUL characters + \paran[in] size number of bytes to copy + */ Fl_String::Fl_String(const char *str, int size) { - init(); - value(str, size); + init_(); + assign(str, size); } -void Fl_String::init() { - size_ = 0; - value_ = 0; - capacity_ = 0; +/** + Destructor. + */ +Fl_String::~Fl_String() { + if (buffer_) + ::free(buffer_); } -/** copy constructor */ -Fl_String::Fl_String(const Fl_String &in) { - init(); - value(in.value(), in.size()); +/** + Copy assignment operator + \param[in] str copy from another Fl_String + \return self + */ +Fl_String &Fl_String::operator=(const Fl_String &str) { + return assign(str); } -/** copy assignment operator */ -Fl_String& Fl_String::operator=(const Fl_String &in) { - if (this == &in) - return *this; - value(in.value(), in.size()); - // debug("copy assigned"); +/** + Assign a C-style string. + \param[in] cstr a NUL terminated C-style string + \return self + */ +Fl_String &Fl_String::operator=(const char *cstr) { + return assign(cstr); +} + +/** + Copy another string. + \param[in] str copy from another Fl_String + \return self + */ +Fl_String &Fl_String::assign(const Fl_String &str) { + if (&str == this) return *this; + return assign(str.data(), str.size()); +} + +/** + Assign a C-style string. + \param[in] cstr a NUL terminated C-style string + \return self + */ +Fl_String &Fl_String::assign(const char *cstr) { + if (cstr && *cstr) { + int len = (int)::strlen(cstr); + return assign(cstr, len); + } else { + resize(0); + } return *this; } -/** assignment operator for 'const char *' */ -Fl_String& Fl_String::operator=(const char *in) { - value(in); - // debug("*STRING* assigned"); +/** + Assign a data block of \p size bytes. + \param[in] str a block of data that may contain NUL characters + \paran[in] size number of bytes to copy + \return self + */ +Fl_String &Fl_String::assign(const char *str, int size) { + if (size > 0) { + grow_(size); + memcpy(buffer_, str, size); + buffer_[size] = 0; + size_ = size; + } else { + resize(0); + } return *this; } -/** Destructor */ -Fl_String::~Fl_String() { - delete[] value_; +// ---- Element Access ------------------------------------------------- MARK: - + +/** + Returns the character at specified bounds checked location. + \param[in] n index of character + \return character at that index, or NUL if out of bounds + */ +char Fl_String::at(int n) const { + if ((n < 0) || (n >= size_)) + return 0; + return operator[](n); } -/** Grow the buffer size to at least size+1 bytes. - By default, this call destroys the contents of the current buffer. - \param size in bytes - \param preserve_text copy existing text into the new buffer +/** + Returns the character at specified location. + \param[in] n index of character + \return character at that index */ -void Fl_String::alloc_buf(int size, bool preserve_text) { - if (size < 0) - return; - if (size > 0 && size <= capacity_) - return; +char Fl_String::operator[](int n) const { + if (buffer_) + return buffer_[n]; + else + return 0; +} - int new_size = (size + 1 + 15) & (~15); // round upwards - char *new_value = new char[new_size]; - capacity_ = new_size - 1; +/** + Returns a reference to the character at specified location. + \param[in] n index of character + \return reference to that character, so it can be used as lvalue + */ +char &Fl_String::operator[](int n) { + if (!buffer_) + reserve(1); + return buffer_[n]; +} - if (preserve_text) { - size_ = (int)strlen(value_); - // the new buffer always has a higher capacity than the old one - // make sure we copy the trailing NUL. - memcpy(new_value, value_, size_+1); - } else { - size_ = 0; - } - delete[] value_; - value_ = new_value; +/** + Return a pointer to the NUL terminated string. + \return reference to non-mutable string + */ +const char *Fl_String::data() const { + if (buffer_) + return buffer_; + else + return &NUL; } -/** Assigns the string value to a C-style string */ -void Fl_String::value(const char *str) { - value(str, str ? (int)strlen(str) : 0); +/** + Return a pointer to the writable NUL terminated string. + \return reference to mutable string + */ +char *Fl_String::data() { + if (!buffer_) + reserve(1); + return buffer_; } -/** Returns the number of non-null bytes in the object */ -int Fl_String::slen() const { - if (!value_) return 0; - return (int)strlen(value_); +/** + Return a pointer to the NUL terminated string. + \return reference to non-mutable string + \note same as `const char *Fl_String::data() const` + */ +const char *Fl_String::c_str() const { + return data(); } -/** Assigns the string value to a buffer of \c len bytes */ -void Fl_String::value(const char *str, int len) { - if (str) { - alloc_buf(len); - size_ = len; - memcpy(value_, str, size_); - value_[size_] = '\0'; - } else { // str == NULL - size_ = 0; // ignore len ! - delete[] value_; // free buffer - value_ = NULL; // set null pointer (!) - capacity_ = 0; // reset capacity - } +// ---- Capacity ------------------------------------------------------- MARK: - + +/** + Checks if the string is empty. + \return true if string contains no data + */ +bool Fl_String::empty() const { + return (size_ == 0); } -/** Returns the minimum capacity of the object */ +/** + Returns the number of bytes in the string. + \return number of bytes in string, not counting trailing NUL + */ +int Fl_String::size() const { + return size_; +} + +/** + Reserve n bytes for storage. + If n is less or equal than size, the capacity is set to size. + \param[in] n requested minimum size, not counting trailing NUL + */ +void Fl_String::reserve(int n) { + grow_(n); +} + +/** + Return the number of chars that are allocated for storage. + \return string capacity, not counting trailing NUL + */ int Fl_String::capacity() const { - return capacity_; // > 0 ? capacity_ - 1 : capacity_; + return capacity_; } -/** Set the minimum capacity to \c num_bytes plus one for a terminating NUL. - The contents of the string buffer will be copied if needed. - \param num_bytes minimum size of buffer +/** + Shrink the capacity to fit the current size. */ -void Fl_String::capacity(int num_bytes) { - alloc_buf(num_bytes, true); +void Fl_String::shrink_to_fit() { + shrink_(size_); } +// ---- Operations ----------------------------------------------------- MARK: - -void Fl_String::release() { - delete[] value_; - value_ = 0; - size_ = 0; - capacity_ = 0; +/** + Set an empty string. + */ +void Fl_String::clear() { + resize(0); +} + +/** + Insert a C-style string or data. + \param[in] at insert at this index + \param[in] src copy bytes from here + \param[in] n_ins optional number of bytes to copy - if not set, copy C-style string + \return self + */ +Fl_String &Fl_String::insert(int at, const char *src, int n_ins) { + if (n_ins == npos) n_ins = src ? (int)::strlen(src) : 0; + return replace_(at, 0, src, n_ins); +} + +/** + Insert another string. + \param[in] at insert at this index + \param[in] src copy string from here + \return self + */ +Fl_String &Fl_String::insert(int at, const Fl_String &src) { + return replace_(at, 0, src.buffer_, src.size_); +} + +/** + Erase some bytes within a string. + \param[in] at erase at this index + \param[in] n_del number of bytes to erase + \return self + */ +Fl_String &Fl_String::erase(int at, int n_del) { + return replace_(at, n_del, NULL, 0); +} + +/** + Append a single character. + \param[in] c append this byte + */ +void Fl_String::push_back(char c) { + replace_(size_, 0, &c, 1); +} + +/** + Remove the last character. + */ +void Fl_String::pop_back() { + replace_(size_-1, 1, NULL, 0); +} + +/** + Append a C-style string or data. + \param[in] src copy bytes from here + \param[in] n_ins optional number of bytes to copy - if not set, copy C-style string + \return self + */ +Fl_String &Fl_String::append(const char *src, int n_ins) { + if (n_ins == npos) n_ins = src ? (int)::strlen(src) : 0; + return replace_(size_, 0, src, n_ins); +} + +/** + Append another string. + \param[in] src copy string from here + \return self + */ +Fl_String &Fl_String::append(const Fl_String &src) { + return replace_(size_, 0, src.buffer_, src.size_); +} + +/** + Append a single byte. + \param[in] c single byte character + \return self + */ +Fl_String &Fl_String::append(char c) { + push_back(c); + return *this; +} + +/** + Append a C-style string or data. + \param[in] src copy C-style string from here + \return self + */ +Fl_String &Fl_String::operator+=(const char *src) { + return append(src); +} + +/** + Append another string. + \param[in] src copy string from here + \return self + */ +Fl_String &Fl_String::operator+=(const Fl_String &src) { + return append(src); +} + +/** + Append a single byte. + \param[in] c single byte character + \return self + */ +Fl_String &Fl_String::operator+=(char c) { + return append(c); +} + +/** + Replace part of the string with a C-style string or data. + \param[in] at erase and insert at this index + \param[in] n_del number of bytes to erase + \param[in] src copy bytes from here + \param[in] n_ins optional number of bytes to copy - if not set, copy C-style string + \return self + */ +Fl_String &Fl_String::replace(int at, int n_del, const char *src, int n_ins) { + if (n_ins == npos) n_ins = src ? (int)::strlen(src) : 0; + return replace_(at, n_del, src, n_ins); +} + +/** + Replace part of the string with another string. + \param[in] at erase and insert at this index + \param[in] n_del number of bytes to erase + \param[in] src copy string from here + \return self + */ +Fl_String &Fl_String::replace(int at, int n_del, const Fl_String &src) { + return replace_(at, n_del, src.buffer_, src.size_); +} + +/** + Return a substring from a string. + \param[in] pos copy string from here - if omitted, copy from start + \param[in] n number of bytes - if omitted, copy all bytes + \return a new string + */ +Fl_String Fl_String::substr(int pos, int n) const { + if (n > size_) n = size_; + int first = pos, last = pos + n; + if ((first < 0) || (first > size_) || (last <= first)) + return Fl_String(); + if (last > size_) last = size_; + return Fl_String(buffer_+first, last-first); +} + +/** + Resizes the string to n characters. + + If \p n is less than the current size, the string will be cropped. If \p n + is more than the current size, the new space will be filled with + NUL characters. + + \param[in] n new size of string + */ +void Fl_String::resize(int n) { + if (n == size_) + return; + if (n < size_) { + size_ = n; + if (buffer_) buffer_[size_] = 0; + } else { + grow_(n); + if (buffer_) ::memset(buffer_+size_, 0, n-size_+1); + } + size_ = n; } -// ============================= DEBUG ============================= +// --- Non Standard ---------------------------------------------------- MARK: - + +/** + Returns the number of bytes until the first NUL byte. + \return number of bytes in C-style string + */ +int Fl_String::strlen() const { + if (!buffer_) return 0; + return (int)::strlen(buffer_); +} /** Write some details about the string to stdout. @@ -163,7 +546,7 @@ void Fl_String::release() { void Fl_String::debug(const char *info) const { if (info) { printf("Fl_String '%-20s': %p, value = %p (%d/%d):\n%s\n", - info, this, value_, size_, capacity_, value_ ? value_ : "<NULL>"); + info, this, buffer_, size_, capacity_, buffer_ ? buffer_ : "<NULL>"); } } @@ -191,7 +574,46 @@ void Fl_String::hexdump(const char *info) const { } else if ((i & 3) == 0) { // separator after 4 bytes printf(" "); } - printf(" %02x", (unsigned char)value_[i]); + printf(" %02x", (unsigned char)buffer_[i]); } printf("\n"); } + +// ---- Non-member functions ------------------------------------------- MARK: - + +/** + Concatenate two strings. + \param[in] lhs first string + \param[in] rhs second string + \return self + */ +Fl_String operator+(const Fl_String &lhs, const Fl_String &rhs) { + Fl_String ret = lhs; + return ret += rhs; +} + +/** + Concatenate two strings. + \param[in] lhs first string + \param[in] rhs second C-style string + \return self + */ +Fl_String operator+(const Fl_String &lhs, const char *rhs) { + Fl_String ret = lhs; + return ret += rhs; +} + +/** + Compare two strings. + \param[in] lhs first string + \param[in] rhs second string + \return true if strings are the same size and have the same content + */ +bool operator==(const Fl_String &lhs, const Fl_String &rhs) { + if (lhs.size() == rhs.size()) { + int sz = lhs.size(); + if (sz == 0) return true; + if (memcmp(lhs.data(), rhs.data(), sz) == 0) return true; + } + return false; +} diff --git a/src/fl_ask.cxx b/src/fl_ask.cxx index 5657cd3b0..6ed6b2667 100644 --- a/src/fl_ask.cxx +++ b/src/fl_ask.cxx @@ -320,7 +320,7 @@ const char *fl_input(const char *fmt, const char *defstr, ...) { /** Shows an input dialog displaying the \p fmt message with variable arguments. - Like fl_input(), but this method has an additional (first) argument \p maxchar + Like fl_input(), but this method has the additional argument \p maxchar that limits the number of \b characters that can be input. Since the string is encoded in UTF-8 it is possible that the number of bytes in the string is larger than \p maxchar. @@ -329,36 +329,56 @@ const char *fl_input(const char *fmt, const char *defstr, ...) { returns the string in an Fl_String object that must be released after use. This can be a local/automatic variable. + The \p ret variable is set to 0 if the user clicked OK, and to a negative + value if the user canceled the dialog. If the dialog was canceled, the returned + string will be empty. + \code #include <FL/fl_ask.H> \endcode Example: \code - { Fl_String str = fl_input_str(0, "Enter text:", ""); - printf("Text is: '%s'\n", str.value() ? str.value() : "<cancelled>"); + { int ret; + Fl_String str = fl_input_str(ret, 0, "Enter text:", ""); + if (ret < 0) + printf("Text input was canceled.\n"); + else + printf("Text is: '%s'\n", str.c_str()); } // (str goes out of scope) \endcode - If the user hits \c Escape or closes the window \c str.value() returns NULL. - + \param[out] ret 0 if user clicked OK, negative if dialog was canceled \param[in] maxchar input size limit in characters (not bytes), use 0 for no limit \param[in] fmt can be used as an sprintf-like format and variables for the message text \param[in] defstr defines the default returned string if no text is entered - \return the user string input if OK was pushed or NULL in Fl_String::value() - \retval Fl_String::value() == NULL if Cancel was pushed or the window was closed by the user + \return the user string input if OK was clicked which can be empty + \return an empty string and set \p ret to a negative value if the user canceled the dialog \since 1.4.0 */ -Fl_String fl_input_str(int maxchar, const char *fmt, const char *defstr, ...) { +Fl_String fl_input_str(int &ret, int maxchar, const char *fmt, const char *defstr, ...) { + Fl_Message msg("?"); + if (maxchar < 0) maxchar = 0; + va_list ap; + va_start(ap, defstr); + const char *r = msg.input_innards(fmt, ap, defstr, FL_NORMAL_INPUT, maxchar); + va_end(ap); + ret = (r == NULL) ? -1 : 0; + return Fl_String(r); +} +/** Shows an input dialog displaying the \p fmt message with variable arguments. + \note No information is given if the user canceled the dialog or clicked OK. + \see fl_input_str(int &ret, int maxchar, const char *label, const char *deflt = 0, ...) + */ +Fl_String fl_input_str(int maxchar, const char *fmt, const char *defstr, ...) { Fl_Message msg("?"); - if (maxchar < 0) - maxchar = 0; + if (maxchar < 0) maxchar = 0; va_list ap; va_start(ap, defstr); const char *r = msg.input_innards(fmt, ap, defstr, FL_NORMAL_INPUT, maxchar); va_end(ap); - return r; // Fl_String(r) + return Fl_String(r); } /** Shows an input dialog displaying the \p fmt message with variable arguments. @@ -378,7 +398,6 @@ Fl_String fl_input_str(int maxchar, const char *fmt, const char *defstr, ...) { \retval NULL if Cancel was pushed or the window was closed by the user */ const char *fl_password(const char *fmt, const char *defstr, ...) { - Fl_Message msg("?"); va_list ap; va_start(ap, defstr); @@ -400,27 +419,42 @@ const char *fl_password(const char *fmt, const char *defstr, ...) { \code #include <FL/fl_ask.H> \endcode - \param[in] maxchar input size limit in characters (not bytes); use 0 for no limit + \param[out] ret 0 if user clicked OK, negative if dialog was canceled + \param[in] maxchar input size limit in characters (not bytes), use 0 for no limit \param[in] fmt can be used as an sprintf-like format and variables for the message text \param[in] defstr defines the default returned string if no text is entered - \return the user string input if OK was pushed or NULL in Fl_String::value() - \retval Fl_String::value() == NULL if Cancel was pushed or the window was closed by the user + \return the user string input if OK was clicked which can be empty + \return an empty string and set \p ret to a negative value if the user canceled the dialog \since 1.4.0 */ -Fl_String fl_password_str(int maxchar, const char *fmt, const char *defstr, ...) { +Fl_String fl_password_str(int &ret, int maxchar, const char *fmt, const char *defstr, ...) { + Fl_Message msg("?"); + if (maxchar < 0) maxchar = 0; + va_list ap; + va_start(ap, defstr); + const char *r = msg.input_innards(fmt, ap, defstr, FL_SECRET_INPUT, maxchar); + va_end(ap); + ret = (r == NULL) ? -1 : 0; + return Fl_String(r); +} +/** Shows an input dialog displaying the \p fmt message with variable arguments. + \note No information is given if the user canceled the dialog or clicked OK. + \see fl_password_str(int &ret, int maxchar, const char *label, const char *deflt = 0, ...) + */ +Fl_String fl_password_str(int maxchar, const char *fmt, const char *defstr, ...) { Fl_Message msg("?"); - if (maxchar < 0) - maxchar = 0; + if (maxchar < 0) maxchar = 0; va_list ap; va_start(ap, defstr); const char *r = msg.input_innards(fmt, ap, defstr, FL_SECRET_INPUT, maxchar); va_end(ap); - return r; // Fl_String(r) + return Fl_String(r); } + /** Sets the preferred position for the message box used in many common dialogs like fl_message(), fl_alert(), fl_ask(), fl_choice(), fl_input(), fl_password(). 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 { |
