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>
This commit is contained in:
Matthias Melcher 2023-02-23 15:42:05 +01:00 committed by GitHub
parent 9281893926
commit 9f87af8ad9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1495 additions and 286 deletions

View File

@ -44,6 +44,12 @@ jobs:
# Execute the build. You can specify a specific target with "--target <NAME>" # Execute the build. You can specify a specific target with "--target <NAME>"
run: cmake --build . --config $BUILD_TYPE run: cmake --build . --config $BUILD_TYPE
- name: Unittest
working-directory: ${{github.workspace}}/build
shell: bash
run: ./bin/test/unittests --core
build-wayland: build-wayland:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -21,94 +21,110 @@
Basic Fl_String class for FLTK. 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. Fl_String is the basic string class for FLTK.
In this version Fl_String can be used to store strings, copy strings, 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 Fl_String always maintains a trailing \e nul byte, but can also contain
if the constructor Fl_String(const char *str, int size) is used. \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 Assignment and copy constructors \b copy the string value such that the
source string can be freed immediately after the assignment. source string can be freed immediately after the assignment.
The string value() can be an empty string \c "" or \c NULL. c_str() and data() can be an empty string \c "", but never be \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.
The method size() returns the full string size, whether the string contains 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() embedded \c nul bytes or not. The special method Fl_String::strlen() returns
is \c NULL, otherwise the same as \c strlen() would do. the length of the string up to the first \e nul.
Examples: All methods of Fl_String work on a byte level. They are not UTF-8 aware,
\code but may hold and manipulate UTF-8 strings if done with care.
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
\since 1.4.0 \since 1.4.0
*/ */
class Fl_String { class Fl_String {
private: 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_; int size_;
char *value_;
int capacity_; int capacity_;
void init_();
void grow_(int n);
void shrink_(int n);
Fl_String &replace_(int at, int n_del, const char *src, int n_ins);
protected:
static const char NUL;
public: public:
static const int npos;
// ---- Assignment
Fl_String(); Fl_String();
Fl_String(const char *str); Fl_String(const Fl_String &str);
Fl_String(const char *cstr);
Fl_String(const char *str, int size); 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);
// copy constructor // ---- Element Access
Fl_String(const Fl_String &in); 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;
// copy assignment operator // ---- Capacity
Fl_String& operator=(const Fl_String &in); bool empty() const;
int size() const;
// assignment operator for 'const char *' void reserve(int n);
Fl_String& operator=(const char *in);
virtual ~Fl_String();
private:
void init();
void alloc_buf(int size, bool preserve_text=false);
void release();
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; int capacity() const;
void capacity(int num_bytes); void shrink_to_fit();
void debug(const char *info = 0) const; // output string info // --- Operations
void hexdump(const char *info = 0) const; // output string info + hexdump 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 }; // 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_ #endif // _FL_Fl_String_H_

View File

@ -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_EXPORT Fl_String fl_input_str(int maxchar, const char *label, const char *deflt = 0, ...)
__fl_attr((__format__(__printf__, 2, 4))); __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_EXPORT Fl_String fl_password_str(int maxchar, const char *label, const char *deflt = 0, ...)
__fl_attr((__format__(__printf__, 2, 4))); __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(); FL_EXPORT Fl_Widget *fl_message_icon();
extern FL_EXPORT Fl_Font fl_message_font_; extern FL_EXPORT Fl_Font fl_message_font_;
extern FL_EXPORT Fl_Fontsize fl_message_size_; extern FL_EXPORT Fl_Fontsize fl_message_size_;

View File

@ -299,7 +299,7 @@ const char* ExternalCodeEditor::tmp_filename() {
static char path[FL_PATH_MAX+1]; static char path[FL_PATH_MAX+1];
const char *tmpdir = create_tmpdir(); const char *tmpdir = create_tmpdir();
if ( !tmpdir ) return 0; 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); snprintf(path, FL_PATH_MAX, "%s/%p%s", tmpdir, (void*)this, ext);
path[FL_PATH_MAX] = 0; path[FL_PATH_MAX] = 0;
return path; return path;
@ -385,7 +385,7 @@ int ExternalCodeEditor::start_editor(const char *editor_cmd,
editor_cmd, filename); editor_cmd, filename);
char cmd[1024]; char cmd[1024];
snprintf(cmd, sizeof(cmd), "%s %s", editor_cmd, filename); snprintf(cmd, sizeof(cmd), "%s %s", editor_cmd, filename);
command_line_.value(editor_cmd); command_line_ = editor_cmd;
open_alert_pipe(); open_alert_pipe();
// Fork editor to background.. // Fork editor to background..
switch ( pid_ = fork() ) { switch ( pid_ = fork() ) {
@ -559,7 +559,7 @@ void ExternalCodeEditor::alert_pipe_cb(FL_SOCKET s, void* d) {
self->last_error_ = 0; self->last_error_ = 0;
if (::read(s, &self->last_error_, sizeof(int)) != sizeof(int)) if (::read(s, &self->last_error_, sizeof(int)) != sizeof(int))
return; return;
const char* cmd = self->command_line_.value(); const char* cmd = self->command_line_.c_str();
if (cmd && *cmd) { if (cmd && *cmd) {
if (cmd[0] == '/') { // is this an absoluet filename? if (cmd[0] == '/') { // is this an absoluet filename?
fl_alert("Can't launch external editor '%s':\n%s\n\ncmd: \"%s\"", fl_alert("Can't launch external editor '%s':\n%s\n\ncmd: \"%s\"",

View File

@ -24,7 +24,7 @@ class ExternalCodeEditor {
time_t file_mtime_; // last modify time of the file (used to determine if file changed) 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) size_t file_size_; // last file size (used to determine if changed)
const char *filename_; const char *filename_;
Fd_String command_line_; Fl_String command_line_;
int last_error_; int last_error_;
int alert_pipe_[2]; int alert_pipe_[2];
bool alert_pipe_open_; bool alert_pipe_open_;

View File

@ -389,7 +389,7 @@ const char* ExternalCodeEditor::tmp_filename() {
static char path[512]; static char path[512];
const char *tmpdir = create_tmpdir(); const char *tmpdir = create_tmpdir();
if ( !tmpdir ) return 0; 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); _snprintf(path, sizeof(path), "%s\\%p%s", tmpdir, (void*)this, ext);
path[sizeof(path)-1] = 0; path[sizeof(path)-1] = 0;
return path; return path;

View File

@ -381,7 +381,7 @@ void Fl_Menu_Item_Type::write_item(Fd_Code_Writer& f) {
switch (g_project.i18n_type) { switch (g_project.i18n_type) {
case 1: case 1:
// we will call i18n when the menu is instantiated for the first time // 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_cstring(label());
f.write_c(")"); f.write_c(")");
break; 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()); f.write_c("%sml->labelb = o->label();\n", f.indent());
} else if (g_project.i18n_type==1) { } else if (g_project.i18n_type==1) {
f.write_c("%sml->labelb = %s(o->label());\n", 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) { } else if (g_project.i18n_type==2) {
f.write_c("%sml->labelb = catgets(%s,%s,i+%d,o->label());\n", 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", f.indent(), g_project.i18n_file[0] ? g_project.i18n_file.c_str() : "_catalog",
g_project.i18n_set.value(), msgnum()); g_project.i18n_set.c_str(), msgnum());
} }
f.write_c("%sml->typea = FL_IMAGE_LABEL;\n", f.indent()); f.write_c("%sml->typea = FL_IMAGE_LABEL;\n", f.indent());
f.write_c("%sml->typeb = FL_NORMAL_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); start_menu_initialiser(f, menuItemInitialized, mname, i);
if (g_project.i18n_type==1) { if (g_project.i18n_type==1) {
f.write_c("%so->label(%s(o->label()));\n", 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) { } else if (g_project.i18n_type==2) {
f.write_c("%so->label(catgets(%s,%s,i+%d,o->label()));\n", 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", f.indent(), g_project.i18n_file[0] ? g_project.i18n_file.c_str() : "_catalog",
g_project.i18n_set.value(), msgnum()); g_project.i18n_set.c_str(), msgnum());
} }
} }
} }

View File

@ -2917,13 +2917,13 @@ void Fl_Widget_Type::write_code1(Fd_Code_Writer& f) {
f.write_cstring(label()); f.write_cstring(label());
break; break;
case 1 : /* GNU gettext */ 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_cstring(label());
f.write_c(")"); f.write_c(")");
break; break;
case 2 : /* POSIX catgets */ case 2 : /* POSIX catgets */
f.write_c("catgets(%s,%s,%d,", g_project.i18n_file[0] ? g_project.i18n_file.value() : "_catalog", f.write_c("catgets(%s,%s,%d,", g_project.i18n_file[0] ? g_project.i18n_file.c_str() : "_catalog",
g_project.i18n_set.value(), msgnum()); g_project.i18n_set.c_str(), msgnum());
f.write_cstring(label()); f.write_cstring(label());
f.write_c(")"); f.write_c(")");
break; break;
@ -2990,13 +2990,13 @@ void Fl_Widget_Type::write_widget_code(Fd_Code_Writer& f) {
f.write_cstring(tooltip()); f.write_cstring(tooltip());
break; break;
case 1 : /* GNU gettext */ 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_cstring(tooltip());
f.write_c(")"); f.write_c(")");
break; break;
case 2 : /* POSIX catgets */ case 2 : /* POSIX catgets */
f.write_c("catgets(%s,%s,%d,", g_project.i18n_file[0] ? g_project.i18n_file.value() : "_catalog", f.write_c("catgets(%s,%s,%d,", g_project.i18n_file[0] ? g_project.i18n_file.c_str() : "_catalog",
g_project.i18n_set.value(), msgnum() + 1); g_project.i18n_set.c_str(), msgnum() + 1);
f.write_cstring(tooltip()); f.write_cstring(tooltip());
f.write_c(")"); f.write_c(")");
break; break;

View File

@ -196,15 +196,15 @@ void show_project_cb(Fl_Widget *, void *) {
use_FL_COMMAND_button->value(g_project.use_FL_COMMAND); use_FL_COMMAND_button->value(g_project.use_FL_COMMAND);
utf8_in_src_button->value(g_project.utf8_in_src); utf8_in_src_button->value(g_project.utf8_in_src);
avoid_early_includes_button->value(g_project.avoid_early_includes); avoid_early_includes_button->value(g_project.avoid_early_includes);
header_file_input->value(g_project.header_file_name); header_file_input->value(g_project.header_file_name.c_str());
code_file_input->value(g_project.code_file_name); code_file_input->value(g_project.code_file_name.c_str());
i18n_type_chooser->value(g_project.i18n_type); i18n_type_chooser->value(g_project.i18n_type);
i18n_function_input->value(g_project.i18n_function); i18n_function_input->value(g_project.i18n_function.c_str());
i18n_static_function_input->value(g_project.i18n_static_function); i18n_static_function_input->value(g_project.i18n_static_function.c_str());
i18n_file_input->value(g_project.i18n_file); i18n_file_input->value(g_project.i18n_file.c_str());
i18n_set_input->value(g_project.i18n_set); i18n_set_input->value(g_project.i18n_set.c_str());
i18n_include_input->value(g_project.i18n_include); i18n_include_input->value(g_project.i18n_include.c_str());
i18n_conditional_input->value(g_project.i18n_conditional); i18n_conditional_input->value(g_project.i18n_conditional.c_str());
switch (g_project.i18n_type) { switch (g_project.i18n_type) {
case 0 : /* None */ case 0 : /* None */
i18n_include_input->hide(); i18n_include_input->hide();
@ -258,12 +258,12 @@ void show_settings_cb(Fl_Widget *, void *) {
} }
void header_input_cb(Fl_Input* i, 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); set_modflag(1);
g_project.header_file_name = i->value(); g_project.header_file_name = i->value();
} }
void code_input_cb(Fl_Input* i, void*) { 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); set_modflag(1);
g_project.code_file_name = i->value(); g_project.code_file_name = i->value();
} }

View File

@ -134,7 +134,7 @@ int write_strings(const char *sfile) {
case 2 : /* POSIX catgets, put a .msg file out */ case 2 : /* POSIX catgets, put a .msg file out */
fprintf(fp, "$ generated by Fast Light User Interface Designer (fluid) version %.4f\n", fprintf(fp, "$ generated by Fast Light User Interface Designer (fluid) version %.4f\n",
FL_VERSION); 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); fputs("$quote \"\n", fp);
for (i = 1, p = Fl_Type::first; p; p = p->next) { 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 (t && g_project.include_H_from_C) {
if (to_sourceview) { if (to_sourceview) {
write_c("#include \"CodeView.h\"\n"); 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)); write_c("#include \"%s\"\n", fl_filename_name(t));
} else { } else {
write_c("#include \"%s\"\n", t); 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]) { if (g_project.i18n_type && g_project.i18n_include[0]) {
int conditional = (g_project.i18n_conditional[0]!=0); int conditional = (g_project.i18n_conditional[0]!=0);
if (conditional) { if (conditional) {
write_c("#ifdef %s\n", g_project.i18n_conditional.value()); write_c("#ifdef %s\n", g_project.i18n_conditional.c_str());
indentation++; indentation++;
} }
if (g_project.i18n_include[0] != '<' && if (g_project.i18n_include[0] != '<' &&
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 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_type == 2) {
if (g_project.i18n_file[0]) { 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 { } else {
write_c("// Initialize I18N stuff now for menus...\n"); write_c("// Initialize I18N stuff now for menus...\n");
write_c("#%sinclude <locale.h>\n", indent()); write_c("#%sinclude <locale.h>\n", indent());
write_c("static char *_locale = setlocale(LC_MESSAGES, \"\");\n"); 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) { if (conditional) {
write_c("#else\n"); write_c("#else\n");
if (g_project.i18n_type == 1) { if (g_project.i18n_type == 1) {
if (g_project.i18n_function[0]) { if (g_project.i18n_function[0]) {
write_c("#%sifndef %s\n", indent(), 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.value()); write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_function.c_str());
write_c("#%sendif\n", indent()); 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"); write_c("#endif\n");
} }
if (g_project.i18n_type == 1 && g_project.i18n_static_function[0]) { 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("#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.value()); write_c("#%sdefine %s(text) text\n", indent_plus(1), g_project.i18n_static_function.c_str());
write_c("#endif\n"); write_c("#endif\n");
} }
} }

View File

@ -771,26 +771,26 @@ int Fd_Project_Writer::write_project(const char *filename, int selected_only) {
write_string("\navoid_early_includes"); write_string("\navoid_early_includes");
if (g_project.i18n_type) { if (g_project.i18n_type) {
write_string("\ni18n_type %d", 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_include"); write_word(g_project.i18n_include.c_str());
write_string("\ni18n_conditional"); write_word(g_project.i18n_conditional); write_string("\ni18n_conditional"); write_word(g_project.i18n_conditional.c_str());
switch (g_project.i18n_type) { switch (g_project.i18n_type) {
case 1 : /* GNU gettext */ case 1 : /* GNU gettext */
write_string("\ni18n_function"); write_word(g_project.i18n_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); write_string("\ni18n_static_function"); write_word(g_project.i18n_static_function.c_str());
break; break;
case 2 : /* POSIX catgets */ case 2 : /* POSIX catgets */
if (g_project.i18n_file[0]) { if (g_project.i18n_file[0]) {
write_string("\ni18n_file"); 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; break;
} }
} }
if (!selected_only) { if (!selected_only) {
write_string("\nheader_name"); write_word(g_project.header_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); write_string("\ncode_name"); write_word(g_project.code_file_name.c_str());
#if 0 #if 0
// https://github.com/fltk/fltk/issues/328 // https://github.com/fltk/fltk/issues/328

View File

@ -164,8 +164,8 @@ int compile_strings = 0; // fluic -cs
int batch_mode = 0; // if set (-c, -u) don't open display int batch_mode = 0; // if set (-c, -u) don't open display
/// command line arguments override settings in the projectfile /// command line arguments override settings in the projectfile
Fd_String g_code_filename_arg; Fl_String g_code_filename_arg;
Fd_String g_header_filename_arg; Fl_String g_header_filename_arg;
/** \var int Fluid_Project::header_file_set /** \var int Fluid_Project::header_file_set
If set, commandline overrides header file name in .fl file. 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 cname[FL_PATH_MAX+1];
char hname[FL_PATH_MAX+1]; char hname[FL_PATH_MAX+1];
g_project.i18n_program = fl_filename_name(filename); g_project.i18n_program = fl_filename_name(filename);
g_project.i18n_program.capacity(FL_PATH_MAX); g_project.i18n_program.resize(FL_PATH_MAX);
fl_filename_setext(g_project.i18n_program.buffer(), FL_PATH_MAX, ""); fl_filename_setext(g_project.i18n_program.data(), FL_PATH_MAX, "");
if (g_project.code_file_name[0] == '.' && strchr(g_project.code_file_name, '/') == NULL) { 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); 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 { } 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); 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 { } 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(); if (!batch_mode) enter_project_dir();
Fd_Code_Writer f; Fd_Code_Writer f;
@ -1805,7 +1806,7 @@ void set_modflag(int mf, int mfc) {
#endif // _WIN32 #endif // _WIN32
else basename = filename; 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_star = modflag ? '*' : ' ';
char mod_c_star = modflag_c ? '*' : ' '; char mod_c_star = modflag_c ? '*' : ' ';
snprintf(title, sizeof(title), "%s%c %s%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); sv_strings->scroll(top, 0);
} else if (sv_source->visible_r() || sv_header->visible_r()) { } else if (sv_source->visible_r() || sv_header->visible_r()) {
g_project.i18n_program = fl_filename_name(sv_source_filename); g_project.i18n_program = fl_filename_name(sv_source_filename);
g_project.i18n_program.capacity(FL_PATH_MAX); g_project.i18n_program.resize(FL_PATH_MAX);
fl_filename_setext(g_project.i18n_program.buffer(), FL_PATH_MAX, ""); fl_filename_setext(g_project.i18n_program.data(), FL_PATH_MAX, "");
Fd_String code_file_name_bak = g_project.code_file_name; 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; 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; g_project.header_file_name = sv_header_filename;
// generate the code and load the files // generate the code and load the files

View File

@ -78,18 +78,6 @@ extern int batch_mode;
extern int pasteoffset; 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 // ---- project settings
class Fluid_Project { class Fluid_Project {
@ -99,27 +87,27 @@ public:
void reset(); void reset();
int i18n_type; int i18n_type;
Fd_String i18n_include; Fl_String i18n_include;
Fd_String i18n_conditional; Fl_String i18n_conditional;
Fd_String i18n_function; Fl_String i18n_function;
Fd_String i18n_static_function; Fl_String i18n_static_function;
Fd_String i18n_file; Fl_String i18n_file;
Fd_String i18n_set; Fl_String i18n_set;
Fd_String i18n_program; Fl_String i18n_program;
int include_H_from_C; int include_H_from_C;
int use_FL_COMMAND; int use_FL_COMMAND;
int utf8_in_src; int utf8_in_src;
int avoid_early_includes; int avoid_early_includes;
int header_file_set; int header_file_set;
int code_file_set; int code_file_set;
Fd_String header_file_name; Fl_String header_file_name;
Fd_String code_file_name; Fl_String code_file_name;
}; };
extern Fluid_Project g_project; extern Fluid_Project g_project;
extern Fd_String g_code_filename_arg; extern Fl_String g_code_filename_arg;
extern Fd_String g_header_filename_arg; extern Fl_String g_header_filename_arg;
// ---- public functions // ---- public functions

View File

@ -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 xywh {95 65 309 20} labelfont 1 labelsize 11 align 4
} { } {
Fl_Input {} { 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 tooltip {The active image for the widget.} xywh {95 65 200 20} labelfont 1 labelsize 11 textsize 11 resizable
} }
Fl_Button {} { 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 {} { Fl_Button {} {
callback shortcut_in_cb 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. 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 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>} code0 {\#include <FL/Fl_Shortcut_Button.H>}

View File

@ -17,137 +17,520 @@
#include <FL/Fl_String.H> #include <FL/Fl_String.H>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <limits.h>
/** \file src/Fl_String.cxx /** \file src/Fl_String.cxx
Basic Fl_String class for FLTK. Basic Fl_String class for FLTK.
*/ */
/** Constructs an empty string */
Fl_String::Fl_String() {
init();
}
/** Constructor from a C-style string */ /*
Fl_String::Fl_String(const char *str) { If buffer_ is NULL, c_str() and buffer() will point here.
init(); */
value(str); const char Fl_String::NUL = 0;
}
/** Constructor from a buffer of \c size bytes */ /**
Fl_String::Fl_String(const char *str, int size) { Indicate a maximum value or error.
init(); This value is generally used as end of string indicator or as the error
value(str, size); indicator by the functions that return a string index.
} */
const int Fl_String::npos = INT_MAX;
void Fl_String::init() { /**
Initialise the class instance.
*/
void Fl_String::init_() {
buffer_ = NULL;
size_ = 0; size_ = 0;
value_ = 0;
capacity_ = 0; capacity_ = 0;
} }
/** copy constructor */ /**
Fl_String::Fl_String(const Fl_String &in) { Grow the buffer to a capacity of at least n bytes.
init();
value(in.value(), in.size());
}
/** copy assignment operator */ This method will always grow the buffer size, or keep it as is, but never
Fl_String& Fl_String::operator=(const Fl_String &in) { shrink it. Use shrink_to_fit() to shrink the buffer as much as possible.
if (this == &in)
return *this;
value(in.value(), in.size());
// debug("copy assigned");
return *this;
}
/** assignment operator for 'const char *' */ \param[in] n number of bytes needed, not counting the trailing NUL
Fl_String& Fl_String::operator=(const char *in) {
value(in);
// debug("*STRING* assigned");
return *this;
}
/** Destructor */
Fl_String::~Fl_String() {
delete[] value_;
}
/** 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
*/ */
void Fl_String::alloc_buf(int size, bool preserve_text) { void Fl_String::grow_(int n) {
if (size < 0) if (n <= capacity_)
return; return;
if (size > 0 && size <= capacity_) int alloc_size_ = n + 1; // trailing NUL
return; // round n up so we can grow in chunks
if (alloc_size_ <= 24) { // allocate at least 24 bytes
int new_size = (size + 1 + 15) & (~15); // round upwards alloc_size_ = 24;
char *new_value = new char[new_size]; } else if (alloc_size_ < 1024) {
capacity_ = new_size - 1; alloc_size_ = (alloc_size_+128) & ~127; // allocate in 128 byte chunks
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 { } else {
size_ = 0; alloc_size_ = (alloc_size_+2048) & ~2047; // allocate in 2k chunks
} }
delete[] value_; // allocate now
value_ = new_value; char *new_buffer = (char*)::malloc(alloc_size_);
} if (buffer_ && (size_ > 0)) {
memcpy(new_buffer, buffer_, size_);
/** Assigns the string value to a C-style string */ ::free(buffer_);
void Fl_String::value(const char *str) {
value(str, str ? (int)strlen(str) : 0);
}
/** Returns the number of non-null bytes in the object */
int Fl_String::slen() const {
if (!value_) return 0;
return (int)strlen(value_);
}
/** 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
} }
if (size_ >= 0)
new_buffer[size_] = 0; // trailing NUL
buffer_ = new_buffer;
capacity_ = alloc_size_-1; // trailing NUL
} }
/** Returns the minimum capacity of the object */ /**
int Fl_String::capacity() const { Shrink the buffer to n bytes, or size, if size > n.
return capacity_; // > 0 ? capacity_ - 1 : capacity_;
}
/** Set the minimum capacity to \c num_bytes plus one for a terminating NUL. Shrink the buffer as much as possible. If \p n is 0 and the string is empty,
The contents of the string buffer will be copied if needed. the buffer will be released.
\param num_bytes minimum size of buffer
\param[in] n shrink buffer to n bytes, not counting the trailing NUL
*/ */
void Fl_String::capacity(int num_bytes) { void Fl_String::shrink_(int n) {
alloc_buf(num_bytes, true); 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;
} }
void Fl_String::release() { // ---- Assignment ----------------------------------------------------- MARK: -
delete[] value_;
value_ = 0; /**
size_ = 0; Allocate an empty string.
capacity_ = 0; */
Fl_String::Fl_String() {
init_();
} }
// ============================= DEBUG ============================= /**
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.
\param[in] cstr a NUL terminated C-style string
*/
Fl_String::Fl_String(const char *cstr) {
init_();
assign(cstr);
}
/**
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_();
assign(str, size);
}
/**
Destructor.
*/
Fl_String::~Fl_String() {
if (buffer_)
::free(buffer_);
}
/**
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);
}
/**
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;
}
/**
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;
}
// ---- 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);
}
/**
Returns the character at specified location.
\param[in] n index of character
\return character at that index
*/
char Fl_String::operator[](int n) const {
if (buffer_)
return buffer_[n];
else
return 0;
}
/**
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];
}
/**
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;
}
/**
Return a pointer to the writable NUL terminated string.
\return reference to mutable string
*/
char *Fl_String::data() {
if (!buffer_)
reserve(1);
return buffer_;
}
/**
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();
}
// ---- 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 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_;
}
/**
Shrink the capacity to fit the current size.
*/
void Fl_String::shrink_to_fit() {
shrink_(size_);
}
// ---- Operations ----------------------------------------------------- MARK: -
/**
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;
}
// --- 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. Write some details about the string to stdout.
@ -163,7 +546,7 @@ void Fl_String::release() {
void Fl_String::debug(const char *info) const { void Fl_String::debug(const char *info) const {
if (info) { if (info) {
printf("Fl_String '%-20s': %p, value = %p (%d/%d):\n%s\n", 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 } else if ((i & 3) == 0) { // separator after 4 bytes
printf(" "); printf(" ");
} }
printf(" %02x", (unsigned char)value_[i]); printf(" %02x", (unsigned char)buffer_[i]);
} }
printf("\n"); 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;
}

View File

@ -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. /** 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 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 string is encoded in UTF-8 it is possible that the number of bytes
in the string is larger than \p maxchar. 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 returns the string in an Fl_String object that must be released after use. This
can be a local/automatic variable. 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 \code #include <FL/fl_ask.H> \endcode
Example: Example:
\code \code
{ Fl_String str = fl_input_str(0, "Enter text:", ""); { int ret;
printf("Text is: '%s'\n", str.value() ? str.value() : "<cancelled>"); 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) } // (str goes out of scope)
\endcode \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] 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] 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 \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() \return the user string input if OK was clicked which can be empty
\retval Fl_String::value() == NULL if Cancel was pushed or the window was closed by the user \return an empty string and set \p ret to a negative value if the user canceled the dialog
\since 1.4.0 \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("?"); Fl_Message msg("?");
if (maxchar < 0) if (maxchar < 0) maxchar = 0;
maxchar = 0;
va_list ap; va_list ap;
va_start(ap, defstr); va_start(ap, defstr);
const char *r = msg.input_innards(fmt, ap, defstr, FL_NORMAL_INPUT, maxchar); const char *r = msg.input_innards(fmt, ap, defstr, FL_NORMAL_INPUT, maxchar);
va_end(ap); va_end(ap);
return r; // Fl_String(r) 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;
va_list ap;
va_start(ap, defstr);
const char *r = msg.input_innards(fmt, ap, defstr, FL_NORMAL_INPUT, maxchar);
va_end(ap);
return Fl_String(r);
} }
/** Shows an input dialog displaying the \p fmt message with variable arguments. /** 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 \retval NULL if Cancel was pushed or the window was closed by the user
*/ */
const char *fl_password(const char *fmt, const char *defstr, ...) { const char *fl_password(const char *fmt, const char *defstr, ...) {
Fl_Message msg("?"); Fl_Message msg("?");
va_list ap; va_list ap;
va_start(ap, defstr); 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 \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] 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 \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() \return the user string input if OK was clicked which can be empty
\retval Fl_String::value() == NULL if Cancel was pushed or the window was closed by the user \return an empty string and set \p ret to a negative value if the user canceled the dialog
\since 1.4.0 \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("?"); Fl_Message msg("?");
if (maxchar < 0) if (maxchar < 0) maxchar = 0;
maxchar = 0;
va_list ap; va_list ap;
va_start(ap, defstr); va_start(ap, defstr);
const char *r = msg.input_innards(fmt, ap, defstr, FL_SECRET_INPUT, maxchar); const char *r = msg.input_innards(fmt, ap, defstr, FL_SECRET_INPUT, maxchar);
va_end(ap); va_end(ap);
return r; // Fl_String(r) 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;
va_list ap;
va_start(ap, defstr);
const char *r = msg.input_innards(fmt, ap, defstr, FL_SECRET_INPUT, maxchar);
va_end(ap);
return Fl_String(r);
}
/** Sets the preferred position for the message box used in /** Sets the preferred position for the message box used in
many common dialogs like fl_message(), fl_alert(), many common dialogs like fl_message(), fl_alert(),
fl_ask(), fl_choice(), fl_input(), fl_password(). fl_ask(), fl_choice(), fl_input(), fl_password().

View File

@ -154,6 +154,7 @@ SET (UNITTEST_SRCS
unittests.h unittests.h
unittest_about.cxx unittest_about.cxx
unittest_points.cxx unittest_points.cxx
unittest_core.cxx
unittest_complex_shapes.cxx unittest_complex_shapes.cxx
unittest_fast_shapes.cxx unittest_fast_shapes.cxx
unittest_circles.cxx unittest_circles.cxx

View File

@ -30,7 +30,8 @@ CPPUNITTEST = \
unittest_viewport.cxx \ unittest_viewport.cxx \
unittest_scrollbarsize.cxx \ unittest_scrollbarsize.cxx \
unittest_schemes.cxx \ unittest_schemes.cxx \
unittest_simple_terminal.cxx unittest_simple_terminal.cxx \
unittest_core.cxx
OBJUNITTEST = \ OBJUNITTEST = \
unittests.o \ unittests.o \

View File

@ -34,16 +34,17 @@
void rename_button(Fl_Widget *o, void *v) { void rename_button(Fl_Widget *o, void *v) {
int what = fl_int(v); int what = fl_int(v);
int ret = 0;
Fl_String input; Fl_String input;
if (what == 0) { if (what == 0) {
fl_message_icon_label("§"); 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 { } else {
fl_message_icon_label(""); 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()) { if (ret == 0) {
o->copy_label(input.value()); o->copy_label(input.c_str());
o->redraw(); o->redraw();
} }
} }

253
test/unittest_core.cxx Normal file
View File

@ -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);

View File

@ -29,10 +29,12 @@
#include <FL/Fl_Double_Window.H> #include <FL/Fl_Double_Window.H>
#include <FL/Fl_Hold_Browser.H> #include <FL/Fl_Hold_Browser.H>
#include <FL/Fl_Help_View.H> #include <FL/Fl_Help_View.H>
#include <FL/Fl_Simple_Terminal.H>
#include <FL/Fl_Group.H> #include <FL/Fl_Group.H>
#include <FL/Fl_Box.H> #include <FL/Fl_Box.H>
#include <FL/fl_draw.H> // fl_text_extents() #include <FL/fl_draw.H> // fl_text_extents()
#include <FL/fl_string_functions.h> // fl_strdup() #include <FL/fl_string_functions.h> // fl_strdup()
#include <FL/fl_ask.H> // fl_message()
#include <stdlib.h> // malloc, free #include <stdlib.h> // malloc, free
class Ut_Main_Window *mainwin = NULL; class Ut_Main_Window *mainwin = NULL;
@ -41,6 +43,8 @@ class Fl_Hold_Browser *browser = NULL;
int UnitTest::num_tests_ = 0; int UnitTest::num_tests_ = 0;
UnitTest *UnitTest::test_list_[200] = { 0 }; UnitTest *UnitTest::test_list_[200] = { 0 };
// ----- UnitTest ------------------------------------------------------ MARK: -
UnitTest::UnitTest(int index, const char* label, Fl_Widget* (*create)()) UnitTest::UnitTest(int index, const char* label, Fl_Widget* (*create)())
: widget_(0L) : widget_(0L)
{ {
@ -77,6 +81,8 @@ void UnitTest::add(int index, UnitTest* t) {
num_tests_ = index+1; num_tests_ = index+1;
} }
// ----- Ut_Main_Window ------------------------------------------------ MARK: -
Ut_Main_Window::Ut_Main_Window(int w, int h, const char *l) Ut_Main_Window::Ut_Main_Window(int w, int h, const char *l)
: Fl_Double_Window(w, h, l), : Fl_Double_Window(w, h, l),
draw_alignment_test_(0) draw_alignment_test_(0)
@ -118,10 +124,265 @@ void Ut_Main_Window::test_alignment(int v) {
redraw(); 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 // 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++ ) { for ( int t=1; t<=browser->size(); t++ ) {
UnitTest* ti = (UnitTest*)browser->data(t); UnitTest* ti = (UnitTest*)browser->data(t);
if ( browser->selected(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 // This is the main call. It creates the window and adds all previously
// registered tests to the browser widget. // registered tests to the browser widget.
int main(int argc, char** argv) { 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::get_system_colors();
Fl::scheme(Fl::scheme()); // init scheme before instantiating tests Fl::scheme(Fl::scheme()); // init scheme before instantiating tests
Fl::visual(FL_RGB); 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 = 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->align(FL_ALIGN_TOP|FL_ALIGN_LEFT);
browser->when(FL_WHEN_CHANGED); 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++) { for (i=0; i<n; i++) {
UnitTest* t = UnitTest::test(i); UnitTest* t = UnitTest::test(i);
if (t) { if (t) {
@ -163,6 +470,6 @@ int main(int argc, char** argv) {
mainwin->show(argc, argv); mainwin->show(argc, argv);
// Select first test in browser, and show that test. // Select first test in browser, and show that test.
browser->select(UT_TEST_ABOUT+1); browser->select(UT_TEST_ABOUT+1);
UT_BROWSER_CB(browser, 0); ui_browser_cb(browser, 0);
return Fl::run(); return Fl::run();
} }

View File

@ -20,6 +20,10 @@
#include <FL/Fl.H> #include <FL/Fl.H>
#include <FL/Fl_Double_Window.H> #include <FL/Fl_Double_Window.H>
#include <stdarg.h>
class Fl_Simple_Terminal;
// WINDOW/WIDGET SIZES // WINDOW/WIDGET SIZES
const int UT_MAINWIN_W = 700; // main window w() const int UT_MAINWIN_W = 700; // main window w()
const int UT_MAINWIN_H = 400; // main window h() const int UT_MAINWIN_H = 400; // main window h()
@ -50,7 +54,8 @@ enum {
UT_TEST_VIEWPORT, UT_TEST_VIEWPORT,
UT_TEST_SCROLLBARSIZE, UT_TEST_SCROLLBARSIZE,
UT_TEST_SCHEMES, 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. // This class helps to automatically register a new test with the unittest app.
@ -74,6 +79,173 @@ private:
static UnitTest* test_list_[]; 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 main window needs an additional drawing feature in order to support
// the viewport alignment test. // the viewport alignment test.
class Ut_Main_Window : public Fl_Double_Window { class Ut_Main_Window : public Fl_Double_Window {