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>"
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

View File

@ -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.
embedded \c nul bytes or not. The special method Fl_String::strlen() returns
the length of the string up to the first \e nul.
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
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_;
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:
static const int npos;
// ---- Assignment
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();
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
Fl_String(const Fl_String &in);
// ---- 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;
// copy assignment operator
Fl_String& operator=(const Fl_String &in);
// 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();
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;
// ---- Capacity
bool empty() const;
int size() const;
void reserve(int n);
int capacity() const;
void capacity(int num_bytes);
void shrink_to_fit();
void debug(const char *info = 0) const; // output string info
void hexdump(const char *info = 0) const; // output string info + hexdump
// --- 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_

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_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_;

View File

@ -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\"",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
} {
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>}

View File

@ -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.
*/
/** Constructs an empty string */
Fl_String::Fl_String() {
init();
}
/** Constructor from a C-style string */
Fl_String::Fl_String(const char *str) {
init();
value(str);
}
/*
If buffer_ is NULL, c_str() and buffer() will point here.
*/
const char Fl_String::NUL = 0;
/** Constructor from a buffer of \c size bytes */
Fl_String::Fl_String(const char *str, int size) {
init();
value(str, size);
}
/**
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;
void Fl_String::init() {
/**
Initialise the class instance.
*/
void Fl_String::init_() {
buffer_ = NULL;
size_ = 0;
value_ = 0;
capacity_ = 0;
}
/** copy constructor */
Fl_String::Fl_String(const Fl_String &in) {
init();
value(in.value(), in.size());
}
/**
Grow the buffer to a capacity of at least n bytes.
/** 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");
return *this;
}
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.
/** assignment operator for 'const char *' */
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
\param[in] n number of bytes needed, not counting the trailing NUL
*/
void Fl_String::alloc_buf(int size, bool preserve_text) {
if (size < 0)
void Fl_String::grow_(int n) {
if (n <= capacity_)
return;
if (size > 0 && size <= capacity_)
return;
int new_size = (size + 1 + 15) & (~15); // round upwards
char *new_value = new char[new_size];
capacity_ = new_size - 1;
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);
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 {
size_ = 0;
alloc_size_ = (alloc_size_+2048) & ~2047; // allocate in 2k chunks
}
delete[] value_;
value_ = new_value;
}
/** Assigns the string value to a C-style string */
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
// 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
}
/** Returns the minimum capacity of the object */
int Fl_String::capacity() const {
return capacity_; // > 0 ? capacity_ - 1 : capacity_;
}
/**
Shrink the buffer to n bytes, or size, if size > n.
/** 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 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::capacity(int num_bytes) {
alloc_buf(num_bytes, true);
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;
}
void Fl_String::release() {
delete[] value_;
value_ = 0;
size_ = 0;
capacity_ = 0;
// ---- Assignment ----------------------------------------------------- MARK: -
/**
Allocate an empty string.
*/
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.
@ -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;
}

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.
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;
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)
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.
@ -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;
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)
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
many common dialogs like fl_message(), fl_alert(),
fl_ask(), fl_choice(), fl_input(), fl_password().

View File

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

View File

@ -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 \

View File

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

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_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();
}

View File

@ -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 {