1 /* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3  *  Copyright (C) 2007-2012  Kouhei Sutou <kou@clear-code.com>
4  *
5  *  This library is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU Lesser General Public License as published by
7  *  the Free Software Foundation, either version 3 of the License, or
8  *  (at your option) any later version.
9  *
10  *  This library is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU Lesser General Public License for more details.
14  *
15  *  You should have received a copy of the GNU Lesser General Public License
16  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  */
19 
20 #ifdef HAVE_CONFIG_H
21 #  include <config.h>
22 #endif /* HAVE_CONFIG_H */
23 
24 #include <stdlib.h>
25 #include <string.h>
26 #include <glib.h>
27 #include <glib/gstdio.h>
28 #include <glib/gi18n-lib.h>
29 #include <gmodule.h>
30 #include <gtk/gtk.h>
31 #include <gdk/gdkkeysyms.h>
32 
33 #include <cutter/cut-module-impl.h>
34 #include <cutter/cut-listener.h>
35 #include <cutter/cut-ui.h>
36 #include <cutter/cut-utils.h>
37 #include <cutter/cut-test-result.h>
38 #include <cutter/cut-test.h>
39 #include <cutter/cut-test-case.h>
40 #include <cutter/cut-verbose-level.h>
41 #include <cutter/cut-enum-types.h>
42 #include <cutter/cut-pipeline.h>
43 #include <cutter/cut-backtrace-entry.h>
44 #include <cutter/cut-logger.h>
45 
46 #define CUT_TYPE_GTK_UI            cut_type_gtk_ui
47 #define CUT_GTK_UI(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), CUT_TYPE_GTK_UI, CutGtkUI))
48 #define CUT_GTK_UI_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), CUT_TYPE_GTK_UI, CutGtkUIClass))
49 #define CUT_IS_GTK_UI(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CUT_TYPE_GTK_UI))
50 #define CUT_IS_GTK_UI_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CUT_TYPE_GTK_UI))
51 #define CUT_GTK_UI_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), CUT_TYPE_GTK_UI, CutGtkUIClass))
52 
53 typedef struct _CutGtkUI CutGtkUI;
54 typedef struct _CutGtkUIClass CutGtkUIClass;
55 
56 typedef struct _RowInfo RowInfo;
57 struct _RowInfo
58 {
59     RowInfo *parent_row_info;
60     CutGtkUI *ui;
61     gchar *path;
62     guint n_tests;
63     guint n_completed_tests;
64     gint pulse;
65     guint update_pulse_id;
66     CutTestResultStatus status;
67 };
68 
69 typedef struct _TestCaseRowInfo
70 {
71     RowInfo row_info;
72     CutTestCase *test_case;
73 } TestCaseRowInfo;
74 
75 typedef struct _TestIteratorRowInfo
76 {
77     RowInfo row_info;
78     TestCaseRowInfo *test_case_row_info;
79     CutTestIterator *test_iterator;
80 } TestIteratorRowInfo;
81 
82 typedef struct TestRowInfo
83 {
84     RowInfo row_info;
85     TestCaseRowInfo *test_case_row_info;
86     CutTest *test;
87 } TestRowInfo;
88 
89 typedef struct IteratedTestRowInfo
90 {
91     RowInfo row_info;
92     TestIteratorRowInfo *test_iterator_row_info;
93     CutIteratedTest *iterated_test;
94     gchar *data_name;
95 } IteratedTestRowInfo;
96 
97 struct _CutGtkUI
98 {
99     GObject        object;
100 
101     GtkWidget     *window;
102     GtkProgressBar *progress_bar;
103     GtkTreeView   *tree_view;
104     GtkTreeStore  *logs;
105     GtkStatusbar  *statusbar;
106     GtkLabel      *summary;
107     GtkWidget     *cancel_or_restart_button;
108 
109     CutRunContext *run_context;
110 
111     gboolean       running;
112     guint          n_tests;
113     guint          n_completed_tests;
114 
115     CutTestResultStatus status;
116 };
117 
118 struct _CutGtkUIClass
119 {
120     GObjectClass parent_class;
121 };
122 
123 enum
124 {
125     PROP_0
126 };
127 
128 enum
129 {
130     COLUMN_COLOR,
131     COLUMN_TEST_STATUS,
132     COLUMN_STATUS_ICON,
133     COLUMN_PROGRESS_TEXT,
134     COLUMN_PROGRESS_VALUE,
135     COLUMN_PROGRESS_PULSE,
136     COLUMN_PROGRESS_VISIBLE,
137     COLUMN_NAME,
138     COLUMN_DESCRIPTION,
139     N_COLUMN
140 };
141 
142 static GType cut_type_gtk_ui = 0;
143 static GObjectClass *parent_class;
144 
145 static void dispose        (GObject         *object);
146 static void set_property   (GObject         *object,
147                             guint            prop_id,
148                             const GValue    *value,
149                             GParamSpec      *pspec);
150 static void get_property   (GObject         *object,
151                             guint            prop_id,
152                             GValue          *value,
153                             GParamSpec      *pspec);
154 
155 static void     attach_to_run_context   (CutListener   *listener,
156                                          CutRunContext *run_context);
157 static void     detach_from_run_context (CutListener   *listener,
158                                          CutRunContext *run_context);
159 static gboolean run                     (CutUI         *ui,
160                                          CutRunContext *run_context);
161 
162 static void
class_init(CutGtkUIClass * klass)163 class_init (CutGtkUIClass *klass)
164 {
165     GObjectClass *gobject_class;
166 
167     parent_class = g_type_class_peek_parent(klass);
168 
169     gobject_class = G_OBJECT_CLASS(klass);
170 
171     gobject_class->dispose      = dispose;
172     gobject_class->set_property = set_property;
173     gobject_class->get_property = get_property;
174 }
175 
176 static void
setup_progress_bar(GtkBox * box,CutGtkUI * ui)177 setup_progress_bar (GtkBox *box, CutGtkUI *ui)
178 {
179     GtkWidget *progress_bar;
180 
181     progress_bar = gtk_progress_bar_new();
182     gtk_box_pack_start(box, progress_bar, TRUE, TRUE, 0);
183 
184     ui->progress_bar = GTK_PROGRESS_BAR(progress_bar);
185     gtk_progress_bar_set_pulse_step(ui->progress_bar, 0.01);
186 }
187 
188 static void
push_message(CutGtkUI * ui,const gchar * context,const gchar * format,...)189 push_message (CutGtkUI *ui, const gchar *context, const gchar *format, ...)
190 {
191     guint context_id;
192     gchar *message;
193     va_list args;
194 
195     context_id = gtk_statusbar_get_context_id(ui->statusbar, context);
196     va_start(args, format);
197     message = g_strdup_vprintf(format, args);
198     va_end(args);
199     gtk_statusbar_push(ui->statusbar, context_id, message);
200     g_free(message);
201 }
202 
203 static void
pop_message(CutGtkUI * ui,const gchar * context)204 pop_message (CutGtkUI *ui, const gchar *context)
205 {
206     guint context_id;
207 
208     context_id = gtk_statusbar_get_context_id(ui->statusbar, context);
209     gtk_statusbar_pop(ui->statusbar, context_id);
210 }
211 
212 static void
remove_all_messages(CutGtkUI * ui,const gchar * context)213 remove_all_messages (CutGtkUI *ui, const gchar *context)
214 {
215 #if GTK_CHECK_VERSION(2, 22, 0)
216     guint context_id;
217 
218     context_id = gtk_statusbar_get_context_id(ui->statusbar, context);
219     gtk_statusbar_remove_all(ui->statusbar, context_id);
220 #endif
221 }
222 
223 static void
run_test(CutGtkUI * ui)224 run_test (CutGtkUI *ui)
225 {
226     ui->n_tests = 0;
227     ui->n_completed_tests = 0;
228     ui->status = CUT_TEST_RESULT_SUCCESS;
229 
230     gtk_tree_store_clear(ui->logs);
231 
232     remove_all_messages(ui, "test");
233     remove_all_messages(ui, "iterated-test");
234     remove_all_messages(ui, "test-suite");
235 
236     cut_run_context_add_listener(ui->run_context, CUT_LISTENER(ui));
237     cut_run_context_start_async(ui->run_context);
238 }
239 
240 static void
cb_cancel_or_restart(GtkToolButton * button,gpointer data)241 cb_cancel_or_restart (GtkToolButton *button, gpointer data)
242 {
243     CutGtkUI *ui = data;
244 
245     if (strcmp(gtk_tool_button_get_stock_id(button), GTK_STOCK_CANCEL) == 0) {
246         gtk_tool_button_set_stock_id(button, GTK_STOCK_REDO);
247         cut_run_context_cancel(ui->run_context);
248     } else {
249         CutRunContext *pipeline;
250 
251         gtk_tool_button_set_stock_id(button, GTK_STOCK_CANCEL);
252 
253         pipeline = cut_pipeline_new_from_run_context(ui->run_context);
254         g_object_unref(ui->run_context);
255         ui->run_context = pipeline;
256         run_test(ui);
257     }
258 }
259 
260 static void
cb_quit(GtkWidget * widget,gpointer data)261 cb_quit (GtkWidget *widget, gpointer data)
262 {
263     CutGtkUI *ui = CUT_GTK_UI(data);
264 
265     gtk_widget_destroy(ui->window);
266 }
267 
268 static void
cb_run_all_tests(GtkWidget * widget,gpointer data)269 cb_run_all_tests (GtkWidget *widget, gpointer data)
270 {
271     CutGtkUI *ui = CUT_GTK_UI(data);
272 
273     cb_cancel_or_restart(GTK_TOOL_BUTTON(ui->cancel_or_restart_button),
274                          (gpointer)ui);
275 }
276 
277 static void
cb_run_test(GtkWidget * widget,gpointer data)278 cb_run_test (GtkWidget *widget, gpointer data)
279 {
280 }
281 
282 #define CUT_WEBSITE_EN "http://cutter.sourceforge.net/"
283 #define CUT_WEBSITE_JA "http://cutter.sourceforge.net/index.html.ja"
284 #define CUT_TUTORIAL_EN "http://cutter.sourceforge.net/reference/tutorial.html"
285 #define CUT_TUTORIAL_JA "http://cutter.sourceforge.net/reference/ja/tutorial.html"
286 #define CUT_REFERENCE_EN "http://cutter.sourceforge.net/reference/"
287 #define CUT_REFERENCE_JA "http://cutter.sourceforge.net/reference/ja/"
288 
289 static void
show_uri_fallback(const gchar * uri)290 show_uri_fallback (const gchar *uri)
291 {
292 #ifdef CUT_FALLBACK_BROWSER
293     GError *error = NULL;
294     GPtrArray *args;
295 
296     args = g_ptr_array_new();
297     g_ptr_array_add(args, CUT_FALLBACK_BROWSER);
298     g_ptr_array_add(args, (gchar *)uri);
299     g_ptr_array_add(args, NULL);
300 
301     g_spawn_async(NULL, (gchar **)args->pdata, NULL,
302                   G_SPAWN_SEARCH_PATH, NULL, NULL, NULL,
303                   &error);
304     g_ptr_array_unref(args);
305 
306     if (!error)
307         return;
308 
309     cut_log_warning("[ui][gtk] failed to show URI (fallback): <%s>: %s",
310                     uri, error->message);
311     g_error_free(error);
312 #endif
313 }
314 
315 static void
show_uri(const gchar * uri)316 show_uri (const gchar *uri)
317 {
318     GError *error = NULL;
319 
320     gtk_show_uri(NULL, uri, gtk_get_current_event_time(), &error);
321     if (!error)
322         return;
323 
324     cut_log_warning("[ui][gtk] failed to show URI: <%s>: %s",
325                     uri, error->message);
326     g_error_free(error);
327 
328     show_uri_fallback(uri);
329 }
330 
331 static void
cb_show_uri(GtkWidget * widget,gpointer data)332 cb_show_uri (GtkWidget *widget, gpointer data)
333 {
334     GtkAction *action;
335     const gchar *name;
336     const gchar *uri = NULL;
337 
338     action = GTK_ACTION(widget);
339     name = gtk_action_get_name(action);
340     if (strcmp(name, "WebsiteEn") == 0) {
341         uri = CUT_WEBSITE_EN;
342     } else if (strcmp(name, "WebsiteJa") == 0) {
343         uri = CUT_WEBSITE_JA;
344     } else if (strcmp(name, "TutorialEn") == 0) {
345         uri = CUT_TUTORIAL_EN;
346     } else if (strcmp(name, "TutorialJa") == 0) {
347         uri = CUT_TUTORIAL_JA;
348     } else if (strcmp(name, "ReferenceEn") == 0) {
349         uri = CUT_REFERENCE_EN;
350     } else if (strcmp(name, "ReferenceJa") == 0) {
351         uri = CUT_REFERENCE_JA;
352     }
353 
354     if (!uri)
355         return;
356 
357     show_uri(uri);
358 }
359 
360 static GtkActionEntry menu_entries[] = {
361     /* name, stock_id, label, accelerator, tooltip, callback */
362     {"FileMenu", NULL, N_("_File"), NULL, "", NULL},
363     {"Quit", GTK_STOCK_QUIT, N_("_Quit"), NULL, "", G_CALLBACK(cb_quit)},
364     {"TestMenu", NULL, N_("_Test"), NULL, "", NULL},
365     {"RunAllTests", GTK_STOCK_REDO, N_("_Run all tests"), NULL, "",
366       G_CALLBACK(cb_run_all_tests)},
367     {"RunTest", GTK_STOCK_REDO, N_("_Run test"), NULL, "",
368       G_CALLBACK(cb_run_test)},
369     {"HelpMenu", NULL, N_("_Help"), NULL, "", NULL},
370     {"Website", NULL, N_("_Website"), NULL, "", NULL},
371     {"WebsiteEn", NULL, N_("_Website English"), NULL, "",
372      G_CALLBACK(cb_show_uri)},
373     {"WebsiteJa", NULL, N_("_Website Japanese"), NULL, "",
374      G_CALLBACK(cb_show_uri)},
375     {"Tutorial", NULL, N_("_Tutorial"), NULL, "", NULL},
376     {"TutorialEn", NULL, N_("_Tutorial English"), NULL, "",
377      G_CALLBACK(cb_show_uri)},
378     {"TutorialJa", NULL, N_("_Tutorial Japanese"), NULL, "",
379      G_CALLBACK(cb_show_uri)},
380     {"Reference", NULL, N_("_Reference"), NULL, "", NULL},
381     {"ReferenceEn", NULL, N_("_Reference English"), NULL, "",
382      G_CALLBACK(cb_show_uri)},
383     {"ReferenceJa", NULL, N_("_Reference Japanese"), NULL, "",
384      G_CALLBACK(cb_show_uri)}
385 };
386 
387 static void
load_actions(CutGtkUI * ui,GtkUIManager * ui_manager)388 load_actions (CutGtkUI *ui, GtkUIManager *ui_manager)
389 {
390     GtkActionGroup *action_group;
391     gint i;
392 
393     action_group = gtk_action_group_new("MenuActions");
394 
395     for (i = 0; i < G_N_ELEMENTS(menu_entries); i++) {
396         gtk_action_group_add_actions(action_group, &(menu_entries[i]), 1, ui);
397     }
398     gtk_ui_manager_insert_action_group(ui_manager, action_group, 0);
399 
400     g_object_unref(action_group);
401 }
402 
403 static void
load_ui_file(GtkUIManager * ui_manager)404 load_ui_file (GtkUIManager *ui_manager)
405 {
406     const gchar *ui_data_dir;
407     gchar *ui_file;
408 
409     ui_data_dir = g_getenv("CUT_UI_DATA_DIR");
410     if (!ui_data_dir) {
411 #ifdef G_OS_WIN32
412         ui_data_dir = cut_win32_ui_data_dir();
413 #else
414         ui_data_dir = UI_DATA_DIR;
415 #endif
416     }
417 
418     ui_file = g_build_filename(ui_data_dir, "gtk-menu.ui", NULL);
419     gtk_ui_manager_add_ui_from_file(ui_manager, ui_file, NULL);
420     g_free(ui_file);
421 }
422 
423 static void
setup_menu_bar(GtkBox * box,CutGtkUI * ui)424 setup_menu_bar (GtkBox *box, CutGtkUI *ui)
425 {
426     GtkUIManager *ui_manager;
427     GtkWidget *menu_bar;
428 
429     ui_manager = gtk_ui_manager_new();
430     load_actions(ui, ui_manager);
431     load_ui_file(ui_manager);
432     gtk_window_add_accel_group(GTK_WINDOW(ui->window),
433                                gtk_ui_manager_get_accel_group(ui_manager));
434 
435     menu_bar = gtk_ui_manager_get_widget(ui_manager, "/menu-bar");
436     if (menu_bar) {
437         gtk_box_pack_start(GTK_BOX(box), menu_bar, FALSE, FALSE, 0);
438     }
439 
440     g_object_unref(ui_manager);
441 }
442 
443 static void
setup_cancel_or_restart_button(GtkToolbar * toolbar,CutGtkUI * ui)444 setup_cancel_or_restart_button (GtkToolbar *toolbar, CutGtkUI *ui)
445 {
446     GtkToolItem *button;
447 
448     button = gtk_tool_button_new_from_stock(GTK_STOCK_CANCEL);
449     gtk_toolbar_insert(toolbar, button, -1);
450 
451     g_signal_connect(button, "clicked",
452                      G_CALLBACK(cb_cancel_or_restart), ui);
453 
454     ui->cancel_or_restart_button = GTK_WIDGET(button);
455 }
456 
457 static void
setup_top_bar(GtkBox * box,CutGtkUI * ui)458 setup_top_bar (GtkBox *box, CutGtkUI *ui)
459 {
460     GtkWidget *hbox, *toolbar;
461 
462     toolbar = gtk_toolbar_new();
463     gtk_toolbar_set_show_arrow(GTK_TOOLBAR(toolbar), FALSE);
464     gtk_box_pack_start(GTK_BOX(box), toolbar, FALSE, TRUE, 0);
465     setup_cancel_or_restart_button(GTK_TOOLBAR(toolbar), ui);
466 
467     hbox = gtk_hbox_new(FALSE, 0);
468     gtk_box_pack_start(box, hbox, FALSE, TRUE, 0);
469 
470     setup_progress_bar(GTK_BOX(hbox), ui);
471 }
472 
473 static void
setup_summary_label(GtkBox * box,CutGtkUI * ui)474 setup_summary_label (GtkBox *box, CutGtkUI *ui)
475 {
476     GtkWidget *summary;
477 
478     summary = gtk_label_new(_("Ready"));
479     gtk_box_pack_start(box, summary, FALSE, TRUE, 0);
480 
481     ui->summary = GTK_LABEL(summary);
482 }
483 
484 static void
setup_all_test_result_view_columns(GtkTreeView * tree_view)485 setup_all_test_result_view_columns (GtkTreeView *tree_view)
486 {
487     GtkCellRenderer *renderer;
488     GtkTreeViewColumn *column;
489 
490     column = gtk_tree_view_column_new();
491 
492     renderer = gtk_cell_renderer_pixbuf_new();
493     gtk_tree_view_column_set_title(column, _("Name"));
494     gtk_tree_view_column_pack_start(column, renderer, FALSE);
495     gtk_tree_view_column_add_attribute(column, renderer,
496                                        "pixbuf", COLUMN_STATUS_ICON);
497 
498     renderer = gtk_cell_renderer_progress_new();
499     gtk_tree_view_column_pack_end(column, renderer, FALSE);
500     gtk_tree_view_column_set_attributes(column, renderer,
501                                         "text", COLUMN_PROGRESS_TEXT,
502                                         "value", COLUMN_PROGRESS_VALUE,
503                                         "pulse", COLUMN_PROGRESS_PULSE,
504                                         "visible", COLUMN_PROGRESS_VISIBLE,
505                                         NULL);
506 
507     renderer = gtk_cell_renderer_text_new();
508     gtk_tree_view_column_pack_start(column, renderer, TRUE);
509     gtk_tree_view_column_set_attributes(column, renderer,
510                                         "text", COLUMN_NAME,
511                                         "background", COLUMN_COLOR,
512                                         NULL);
513     gtk_tree_view_column_set_sort_column_id(column, COLUMN_NAME);
514     gtk_tree_view_append_column(tree_view, column);
515 
516     renderer = g_object_new(GTK_TYPE_CELL_RENDERER_TEXT,
517                             "font", "Monospace",
518                             NULL);
519     column = gtk_tree_view_column_new_with_attributes(_("Description"), renderer,
520                                                       "text", COLUMN_DESCRIPTION,
521                                                       "background", COLUMN_COLOR,
522                                                       NULL);
523     gtk_tree_view_append_column(tree_view, column);
524 }
525 
526 static GtkWidget *
create_all_test_result_view(CutGtkUI * ui)527 create_all_test_result_view (CutGtkUI *ui)
528 {
529     GtkWidget *tree_view, *scrolled_window;
530     GtkTreeStore *tree_store;
531 
532     scrolled_window = gtk_scrolled_window_new(NULL, NULL);
533     gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window),
534                                    GTK_POLICY_AUTOMATIC,
535                                    GTK_POLICY_AUTOMATIC);
536 
537     tree_store = gtk_tree_store_new(N_COLUMN,
538                                     G_TYPE_STRING,
539                                     G_TYPE_INT,
540                                     GDK_TYPE_PIXBUF,
541                                     G_TYPE_STRING,
542                                     G_TYPE_INT,
543                                     G_TYPE_INT,
544                                     G_TYPE_BOOLEAN,
545                                     G_TYPE_STRING, G_TYPE_STRING);
546     ui->logs = tree_store;
547 
548     tree_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(tree_store));
549     gtk_container_add(GTK_CONTAINER(scrolled_window), tree_view);
550     ui->tree_view = GTK_TREE_VIEW(tree_view);
551     setup_all_test_result_view_columns(ui->tree_view);
552 
553     return scrolled_window;
554 }
555 
556 static GtkWidget *
create_partial_test_result_view(CutGtkUI * ui)557 create_partial_test_result_view (CutGtkUI *ui)
558 {
559     GtkWidget *scrolled_window;
560 
561     scrolled_window = gtk_scrolled_window_new(NULL, NULL);
562     return scrolled_window;
563 }
564 
565 static void
setup_test_result_view(GtkBox * box,CutGtkUI * ui)566 setup_test_result_view (GtkBox *box, CutGtkUI *ui)
567 {
568     GtkWidget *hpaned;
569     GtkWidget *all_test_result_view, *partial_test_result_view;
570 
571     hpaned = gtk_hpaned_new();
572 
573     all_test_result_view = create_all_test_result_view(ui);
574     partial_test_result_view = create_partial_test_result_view(ui);
575 
576     gtk_paned_pack1(GTK_PANED(hpaned), all_test_result_view, TRUE, TRUE);
577     gtk_paned_pack2(GTK_PANED(hpaned), partial_test_result_view, TRUE, TRUE);
578 
579     gtk_box_pack_start(box, hpaned, TRUE, TRUE, 0);
580 }
581 
582 static void
setup_statusbar(GtkBox * box,CutGtkUI * ui)583 setup_statusbar (GtkBox *box, CutGtkUI *ui)
584 {
585     GtkWidget *statusbar;
586 
587     statusbar = gtk_statusbar_new();
588     gtk_box_pack_start(box, statusbar, FALSE, FALSE, 0);
589     ui->statusbar = GTK_STATUSBAR(statusbar);
590 }
591 
592 static gboolean
cb_destroy(GtkWidget * widget,gpointer data)593 cb_destroy (GtkWidget *widget, gpointer data)
594 {
595     CutGtkUI *ui = data;
596 
597     ui->window = NULL;
598     gtk_main_quit();
599 
600     return TRUE;
601 }
602 
603 static gboolean
cb_key_press_event(GtkWidget * widget,GdkEventKey * event,gpointer data)604 cb_key_press_event (GtkWidget *widget, GdkEventKey *event, gpointer data)
605 {
606     if (event->state == GDK_CONTROL_MASK && event->keyval == GDK_q) {
607         gtk_widget_destroy(widget);
608         return TRUE;
609     }
610 
611     return FALSE;
612 }
613 
614 static void
setup_window(CutGtkUI * ui)615 setup_window (CutGtkUI *ui)
616 {
617     GtkWidget *window, *vbox;
618 
619     window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
620     gtk_window_set_default_size(GTK_WINDOW(window), 600, 500);
621     gtk_window_set_title(GTK_WINDOW(window), "Cutter");
622 
623     g_signal_connect(window, "destroy",
624                      G_CALLBACK(cb_destroy), ui);
625     g_signal_connect(window, "key-press-event",
626                      G_CALLBACK(cb_key_press_event), NULL);
627 
628     ui->window = window;
629 
630     vbox = gtk_vbox_new(FALSE, 0);
631     gtk_container_add(GTK_CONTAINER(window), vbox);
632 
633     setup_menu_bar(GTK_BOX(vbox), ui);
634     setup_top_bar(GTK_BOX(vbox), ui);
635     setup_summary_label(GTK_BOX(vbox), ui);
636     setup_test_result_view(GTK_BOX(vbox), ui);
637     setup_statusbar(GTK_BOX(vbox), ui);
638 
639     gtk_window_set_focus(GTK_WINDOW(window), GTK_WIDGET(ui->tree_view));
640 }
641 
642 static void
init(CutGtkUI * ui)643 init (CutGtkUI *ui)
644 {
645     ui->run_context = NULL;
646     ui->n_tests = 0;
647     ui->n_completed_tests = 0;
648     ui->status = CUT_TEST_RESULT_SUCCESS;
649 
650     setup_window(ui);
651 }
652 
653 static void
listener_init(CutListenerClass * listener)654 listener_init (CutListenerClass *listener)
655 {
656     listener->attach_to_run_context   = attach_to_run_context;
657     listener->detach_from_run_context = detach_from_run_context;
658 }
659 
660 static void
ui_init(CutUIClass * ui)661 ui_init (CutUIClass *ui)
662 {
663     ui->run = run;
664 }
665 
666 static void
register_type(GTypeModule * type_module)667 register_type (GTypeModule *type_module)
668 {
669     static const GTypeInfo info =
670         {
671             sizeof (CutGtkUIClass),
672             (GBaseInitFunc) NULL,
673             (GBaseFinalizeFunc) NULL,
674             (GClassInitFunc) class_init,
675             NULL,           /* class_finalize */
676             NULL,           /* class_data */
677             sizeof(CutGtkUI),
678             0,
679             (GInstanceInitFunc) init,
680         };
681 
682     static const GInterfaceInfo ui_info =
683         {
684             (GInterfaceInitFunc) ui_init,
685             NULL,
686             NULL
687         };
688 
689     static const GInterfaceInfo listener_info =
690         {
691             (GInterfaceInitFunc) listener_init,
692             NULL,
693             NULL
694         };
695 
696     cut_type_gtk_ui = g_type_module_register_type(type_module,
697                                                   G_TYPE_OBJECT,
698                                                   "CutGtkUI",
699                                                   &info, 0);
700 
701     g_type_module_add_interface(type_module,
702                                 cut_type_gtk_ui,
703                                 CUT_TYPE_UI,
704                                 &ui_info);
705 
706     g_type_module_add_interface(type_module,
707                                 cut_type_gtk_ui,
708                                 CUT_TYPE_LISTENER,
709                                 &listener_info);
710 }
711 
712 G_MODULE_EXPORT GList *
CUT_MODULE_IMPL_INIT(GTypeModule * type_module)713 CUT_MODULE_IMPL_INIT (GTypeModule *type_module)
714 {
715     GList *registered_types = NULL;
716 
717     register_type(type_module);
718     if (cut_type_gtk_ui)
719         registered_types =
720             g_list_prepend(registered_types,
721                            (gchar *)g_type_name(cut_type_gtk_ui));
722 
723     return registered_types;
724 }
725 
726 G_MODULE_EXPORT void
CUT_MODULE_IMPL_EXIT(void)727 CUT_MODULE_IMPL_EXIT (void)
728 {
729 }
730 
731 G_MODULE_EXPORT GObject *
CUT_MODULE_IMPL_INSTANTIATE(const gchar * first_property,va_list var_args)732 CUT_MODULE_IMPL_INSTANTIATE (const gchar *first_property, va_list var_args)
733 {
734     return g_object_new_valist(CUT_TYPE_GTK_UI, first_property, var_args);
735 }
736 
737 static void
dispose(GObject * object)738 dispose (GObject *object)
739 {
740     CutGtkUI *ui = CUT_GTK_UI(object);
741 
742     if (ui->logs) {
743         g_object_unref(ui->logs);
744         ui->logs = NULL;
745     }
746     if (ui->window) {
747         gtk_widget_destroy(ui->window);
748         ui->window = NULL;
749     }
750 
751     if (ui->run_context) {
752         g_object_unref(ui->run_context);
753         ui->run_context = NULL;
754     }
755 
756     G_OBJECT_CLASS(parent_class)->dispose(object);
757 }
758 
759 static void
set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)760 set_property (GObject      *object,
761               guint         prop_id,
762               const GValue *value,
763               GParamSpec   *pspec)
764 {
765     switch (prop_id) {
766       default:
767         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
768         break;
769     }
770 }
771 
772 static void
get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)773 get_property (GObject    *object,
774               guint       prop_id,
775               GValue     *value,
776               GParamSpec *pspec)
777 {
778     switch (prop_id) {
779       default:
780         G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
781         break;
782     }
783 }
784 
785 static const gchar *
status_to_color(CutTestResultStatus status,gboolean only_if_not_success)786 status_to_color (CutTestResultStatus status, gboolean only_if_not_success)
787 {
788     const gchar *color = "white";
789 
790     switch (status) {
791       case CUT_TEST_RESULT_SUCCESS:
792         if (only_if_not_success)
793             color = NULL;
794         else
795             color = "light green";
796         break;
797       case CUT_TEST_RESULT_NOTIFICATION:
798         color = "light blue";
799         break;
800       case CUT_TEST_RESULT_OMISSION:
801         color = "blue";
802         break;
803       case CUT_TEST_RESULT_PENDING:
804         color = "yellow";
805         break;
806       case CUT_TEST_RESULT_FAILURE:
807         color = "red";
808         break;
809       case CUT_TEST_RESULT_ERROR:
810         color = "purple";
811         break;
812       case CUT_TEST_RESULT_CRASH:
813         color = "red";
814         break;
815       default:
816         break;
817     }
818 
819     return color;
820 }
821 
822 static GdkPixbuf *
get_status_icon_by_id(GtkTreeView * tree_view,const gchar * stock_id)823 get_status_icon_by_id (GtkTreeView *tree_view, const gchar *stock_id)
824 {
825     return gtk_widget_render_icon(GTK_WIDGET(tree_view),
826                                   stock_id, GTK_ICON_SIZE_MENU,
827                                   NULL);
828 }
829 
830 static GdkPixbuf *
get_status_icon(GtkTreeView * tree_view,CutTestResultStatus status)831 get_status_icon (GtkTreeView *tree_view, CutTestResultStatus status)
832 {
833     const gchar *stock_id = "";
834 
835     switch (status) {
836     case CUT_TEST_RESULT_SUCCESS:
837         stock_id = GTK_STOCK_YES;
838         break;
839     case CUT_TEST_RESULT_NOTIFICATION:
840         stock_id = GTK_STOCK_DIALOG_WARNING;
841         break;
842     case CUT_TEST_RESULT_OMISSION:
843         stock_id = GTK_STOCK_DIALOG_ERROR;
844         break;
845     case CUT_TEST_RESULT_PENDING:
846         stock_id = GTK_STOCK_DIALOG_ERROR;
847         break;
848     case CUT_TEST_RESULT_FAILURE:
849         stock_id = GTK_STOCK_STOP;
850         break;
851     case CUT_TEST_RESULT_ERROR:
852         stock_id = GTK_STOCK_CANCEL;
853         break;
854     case CUT_TEST_RESULT_CRASH:
855         stock_id = GTK_STOCK_STOP;
856         break;
857     default:
858         stock_id = GTK_STOCK_INFO;
859         break;
860     }
861 
862     return get_status_icon_by_id(tree_view, stock_id);
863 }
864 
865 static gchar *
generate_summary_message(CutRunContext * run_context)866 generate_summary_message (CutRunContext *run_context)
867 {
868     guint n_tests, n_assertions, n_failures, n_errors;
869     guint n_pendings, n_notifications, n_omissions;
870 
871     n_tests = cut_run_context_get_n_tests(run_context);
872     n_assertions = cut_run_context_get_n_assertions(run_context);
873     n_failures = cut_run_context_get_n_failures(run_context);
874     n_errors = cut_run_context_get_n_errors(run_context);
875     n_pendings = cut_run_context_get_n_pendings(run_context);
876     n_notifications = cut_run_context_get_n_notifications(run_context);
877     n_omissions = cut_run_context_get_n_omissions(run_context);
878 
879     return g_strdup_printf(_("%d test(s), %d assertion(s), %d failure(s), "
880                              "%d error(s), %d pending(s), %d omission(s), "
881                              "%d notification(s)"),
882                            n_tests, n_assertions, n_failures, n_errors,
883                            n_pendings, n_omissions, n_notifications);
884 }
885 
886 static gchar *
generate_short_summary_message(CutRunContext * run_context)887 generate_short_summary_message (CutRunContext *run_context)
888 {
889     guint n_tests, n_assertions, n_failures, n_errors;
890     guint n_pendings, n_notifications, n_omissions;
891 
892     n_tests = cut_run_context_get_n_tests(run_context);
893     n_assertions = cut_run_context_get_n_assertions(run_context);
894     n_failures = cut_run_context_get_n_failures(run_context);
895     n_errors = cut_run_context_get_n_errors(run_context);
896     n_pendings = cut_run_context_get_n_pendings(run_context);
897     n_notifications = cut_run_context_get_n_notifications(run_context);
898     n_omissions = cut_run_context_get_n_omissions(run_context);
899 
900     return g_strdup_printf(_("%dT:%dA:%dF:%d:E:%dP:%dO:%dN"),
901                            n_tests, n_assertions, n_failures, n_errors,
902                            n_pendings, n_omissions, n_notifications);
903 }
904 
905 static void
update_button_sensitive(CutGtkUI * ui)906 update_button_sensitive (CutGtkUI *ui)
907 {
908     GtkToolButton *button;
909 
910     button = GTK_TOOL_BUTTON(ui->cancel_or_restart_button);
911     if (ui->running) {
912         gtk_tool_button_set_stock_id(button, GTK_STOCK_CANCEL);
913     } else {
914         gtk_tool_button_set_stock_id(button, GTK_STOCK_REDO);
915     }
916 }
917 
918 static void
update_progress_color(GtkProgressBar * bar,CutTestResultStatus status)919 update_progress_color (GtkProgressBar *bar, CutTestResultStatus status)
920 {
921     GtkStyle *style;
922 
923     style = gtk_style_new();
924     gdk_color_parse(status_to_color(status, FALSE),
925                     &(style->bg[GTK_STATE_PRELIGHT]));
926     gtk_widget_set_style(GTK_WIDGET(bar), style);
927     g_object_unref(style);
928 }
929 
930 static void
update_progress_bar(CutGtkUI * ui)931 update_progress_bar (CutGtkUI *ui)
932 {
933     GtkProgressBar *bar;
934     guint n_tests, n_completed_tests;
935 
936     n_tests = ui->n_tests;
937     n_completed_tests = ui->n_completed_tests;
938     bar = ui->progress_bar;
939 
940     update_progress_color(bar, ui->status);
941 
942     if (n_tests > 0) {
943         gdouble fraction;
944         gchar *text;
945 
946         fraction = n_completed_tests / (gdouble)n_tests;
947         gtk_progress_bar_set_fraction(ui->progress_bar, fraction);
948 
949         text = g_strdup_printf(_("%u/%u (%u%%): %.1fs"),
950                                n_completed_tests, n_tests,
951                                (guint)(fraction * 100),
952                                cut_run_context_get_elapsed(ui->run_context));
953         gtk_progress_bar_set_text(bar, text);
954         g_free(text);
955     } else {
956         gtk_progress_bar_pulse(bar);
957     }
958 }
959 
960 static void
cb_ready_test_suite(CutRunContext * run_context,CutTestSuite * test_suite,guint n_test_cases,guint n_tests,CutGtkUI * ui)961 cb_ready_test_suite (CutRunContext *run_context, CutTestSuite *test_suite,
962                      guint n_test_cases, guint n_tests, CutGtkUI *ui)
963 {
964     ui->running = TRUE;
965     ui->n_tests = n_tests;
966 
967     update_button_sensitive(ui);
968     push_message(ui, "test-suite",
969                  _("Starting test suite %s..."),
970                  cut_test_get_name(CUT_TEST(test_suite)));
971 }
972 
973 static void
update_row_status(RowInfo * row_info)974 update_row_status (RowInfo *row_info)
975 {
976     CutGtkUI *ui;
977     GtkTreeIter iter;
978 
979     ui = row_info->ui;
980     if (gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(ui->logs),
981                                             &iter, row_info->path)) {
982         GdkPixbuf *icon;
983         icon = get_status_icon(ui->tree_view, row_info->status);
984         gtk_tree_store_set(ui->logs, &iter,
985                            COLUMN_STATUS_ICON, icon,
986                            -1);
987         g_object_unref(icon);
988     }
989 }
990 
991 static void
free_test_case_row_info(TestCaseRowInfo * info)992 free_test_case_row_info (TestCaseRowInfo *info)
993 {
994     RowInfo *row_info;
995 
996     g_object_unref(info->test_case);
997 
998     row_info = &(info->row_info);
999     g_object_unref(row_info->ui);
1000     g_free(row_info->path);
1001 
1002     g_free(info);
1003 }
1004 
1005 static void
disable_progress(RowInfo * row_info)1006 disable_progress (RowInfo *row_info)
1007 {
1008     CutGtkUI *ui;
1009     GtkTreeIter iter;
1010 
1011     if (row_info->update_pulse_id) {
1012         g_source_remove(row_info->update_pulse_id);
1013         row_info->update_pulse_id = 0;
1014     }
1015 
1016     ui = row_info->ui;
1017     if (gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(ui->logs),
1018                                             &iter, row_info->path)) {
1019         gtk_tree_store_set(ui->logs, &iter,
1020                            COLUMN_PROGRESS_VISIBLE, FALSE,
1021                            COLUMN_PROGRESS_PULSE, -1,
1022                            -1);
1023     }
1024 }
1025 
1026 static void
free_test_row_info(TestRowInfo * info)1027 free_test_row_info (TestRowInfo *info)
1028 {
1029     RowInfo *row_info;
1030 
1031     row_info = &(info->row_info);
1032     disable_progress(row_info);
1033 
1034     g_object_unref(info->test);
1035     g_object_unref(row_info->ui);
1036     g_free(row_info->path);
1037 
1038     g_free(info);
1039 }
1040 
1041 static void
update_status(RowInfo * row_info,CutTestResultStatus status)1042 update_status (RowInfo *row_info, CutTestResultStatus status)
1043 {
1044     RowInfo *parent_row_info;
1045     CutGtkUI *ui;
1046 
1047     ui = row_info->ui;
1048     row_info->status = status;
1049 
1050     for (parent_row_info = row_info->parent_row_info;
1051          parent_row_info;
1052          parent_row_info = parent_row_info->parent_row_info) {
1053         if (parent_row_info->status < status) {
1054             parent_row_info->status = status;
1055         } else {
1056             break;
1057         }
1058     }
1059 
1060     if (ui->status < status)
1061         ui->status = status;
1062 }
1063 
1064 static gchar *
append_row(CutGtkUI * ui,const gchar * parent_path,const gchar * name,const gchar * description)1065 append_row (CutGtkUI *ui, const gchar *parent_path,
1066             const gchar *name, const gchar *description)
1067 {
1068     GtkTreeIter iter;
1069     GdkPixbuf *icon;
1070 
1071     if (parent_path) {
1072         GtkTreeIter parent_iter;
1073         gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(ui->logs),
1074                                             &parent_iter,
1075                                             parent_path);
1076         gtk_tree_store_append(ui->logs, &iter, &parent_iter);
1077     } else {
1078         gtk_tree_store_append(ui->logs, &iter, NULL);
1079     }
1080 
1081     icon = get_status_icon_by_id(ui->tree_view, GTK_STOCK_MEDIA_PLAY);
1082     gtk_tree_store_set(ui->logs, &iter,
1083                        COLUMN_NAME, name,
1084                        COLUMN_DESCRIPTION, description,
1085                        COLUMN_PROGRESS_PULSE, -1,
1086                        COLUMN_PROGRESS_VISIBLE, TRUE,
1087                        COLUMN_STATUS_ICON, icon,
1088                        -1);
1089     g_object_unref(icon);
1090 
1091     return gtk_tree_model_get_string_from_iter(GTK_TREE_MODEL(ui->logs),
1092                                                &iter);
1093 }
1094 
1095 static void
update_row(CutGtkUI * ui,RowInfo * row_info)1096 update_row (CutGtkUI *ui, RowInfo *row_info)
1097 {
1098     GtkTreeIter iter;
1099 
1100     if (gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(ui->logs),
1101                                             &iter, row_info->path)) {
1102         gdouble fraction;
1103         gint percent;
1104         gchar *text;
1105         GdkPixbuf *icon;
1106 
1107         fraction = row_info->n_completed_tests / (gdouble)(row_info->n_tests);
1108         percent = (gint)(fraction * 100);
1109         text = g_strdup_printf("%d/%d (%d%%)",
1110                                row_info->n_completed_tests,
1111                                row_info->n_tests, percent);
1112         icon = get_status_icon(ui->tree_view, row_info->status);
1113         gtk_tree_store_set(ui->logs, &iter,
1114                            COLUMN_PROGRESS_TEXT, text,
1115                            COLUMN_PROGRESS_VALUE, percent,
1116                            COLUMN_STATUS_ICON, icon,
1117                            COLUMN_COLOR, status_to_color(row_info->status, TRUE),
1118                            -1);
1119         g_free(text);
1120         g_object_unref(icon);
1121     }
1122 }
1123 
1124 static gboolean
timeout_cb_pulse_test(gpointer data)1125 timeout_cb_pulse_test (gpointer data)
1126 {
1127     RowInfo *row_info = data;
1128     CutGtkUI *ui;
1129     GtkTreeIter iter;
1130 
1131     ui = row_info->ui;
1132     row_info->pulse++;
1133     if (gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(ui->logs),
1134                                             &iter, row_info->path)) {
1135         gtk_tree_store_set(ui->logs, &iter,
1136                            COLUMN_PROGRESS_PULSE, row_info->pulse,
1137                            -1);
1138     }
1139     return ui->running;
1140 }
1141 
1142 static void
expand_row(CutGtkUI * ui,const gchar * path)1143 expand_row (CutGtkUI *ui, const gchar *path)
1144 {
1145     GtkTreePath *tree_path;
1146 
1147     tree_path = gtk_tree_path_new_from_string(path);
1148     gtk_tree_view_expand_to_path(ui->tree_view, tree_path);
1149     gtk_tree_view_scroll_to_cell(ui->tree_view, tree_path, NULL, TRUE, 0, 0.5);
1150     gtk_tree_path_free(tree_path);
1151 }
1152 
1153 static void
update_test_row_status(RowInfo * row_info)1154 update_test_row_status (RowInfo *row_info)
1155 {
1156     CutGtkUI *ui;
1157     GtkTreeIter iter;
1158 
1159     ui = row_info->ui;
1160 
1161     if (gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(ui->logs),
1162                                             &iter, row_info->path)) {
1163         GdkPixbuf *icon;
1164         icon = get_status_icon(ui->tree_view, row_info->status);
1165         gtk_tree_store_set(ui->logs, &iter,
1166                            COLUMN_STATUS_ICON, icon,
1167                            COLUMN_PROGRESS_VISIBLE, FALSE,
1168                            COLUMN_COLOR, status_to_color(row_info->status, TRUE),
1169                            -1);
1170         g_object_unref(icon);
1171 
1172         if (row_info->status != CUT_TEST_RESULT_SUCCESS) {
1173             GtkTreePath *path;
1174             path = gtk_tree_model_get_path(GTK_TREE_MODEL(ui->logs), &iter);
1175             gtk_tree_view_expand_to_path(ui->tree_view, path);
1176             gtk_tree_view_scroll_to_cell(ui->tree_view, path, NULL,
1177                                          TRUE, 0, 0.5);
1178             gtk_tree_path_free(path);
1179         }
1180     }
1181 }
1182 
1183 
1184 static void
append_test_result_row_under(CutGtkUI * ui,CutTestResult * result,GtkTreeIter * test_row_iter,GtkTreeIter * result_row_iter)1185 append_test_result_row_under (CutGtkUI *ui, CutTestResult *result,
1186                               GtkTreeIter *test_row_iter,
1187                               GtkTreeIter *result_row_iter)
1188 {
1189     CutTestResultStatus status;
1190     const GList *node;
1191     GString *name;
1192     const gchar *message;
1193     GdkPixbuf *icon;
1194 
1195     status = cut_test_result_get_status(result);
1196     message = cut_test_result_get_message(result);
1197 
1198     name = g_string_new(NULL);
1199     for (node = cut_test_result_get_backtrace(result);
1200          node;
1201          node = g_list_next(node)) {
1202         CutBacktraceEntry *entry = node->data;
1203 
1204         cut_backtrace_entry_format_string(entry, name);
1205         if (g_list_next(node))
1206             g_string_append(name, "\n");
1207     }
1208 
1209     icon = get_status_icon(ui->tree_view, status);
1210 
1211     gtk_tree_store_append(ui->logs, result_row_iter, test_row_iter);
1212     gtk_tree_store_set(ui->logs, result_row_iter,
1213                        COLUMN_NAME, name->str,
1214                        COLUMN_DESCRIPTION, message,
1215                        COLUMN_STATUS_ICON, icon,
1216                        COLUMN_COLOR, status_to_color(status, TRUE),
1217                        -1);
1218     g_string_free(name, TRUE);
1219     g_object_unref(icon);
1220 }
1221 
1222 static void
append_test_result_row(RowInfo * row_info,CutTestResult * result)1223 append_test_result_row (RowInfo *row_info, CutTestResult *result)
1224 {
1225     CutGtkUI *ui;
1226     GtkTreeIter test_row_iter;
1227 
1228     ui = row_info->ui;
1229     if (gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(ui->logs),
1230                                             &test_row_iter, row_info->path)) {
1231         GtkTreePath *path;
1232         GtkTreeIter iter;
1233 
1234         append_test_result_row_under(ui, result, &test_row_iter, &iter);
1235 
1236         path = gtk_tree_model_get_path(GTK_TREE_MODEL(ui->logs), &iter);
1237         gtk_tree_view_expand_to_path(ui->tree_view, path);
1238         gtk_tree_view_scroll_to_cell(ui->tree_view, path, NULL,
1239                                      TRUE, 0, 0.5);
1240         gtk_tree_path_free(path);
1241     }
1242 
1243     g_object_unref(result);
1244 }
1245 
1246 static void
update_summary(CutGtkUI * ui)1247 update_summary (CutGtkUI *ui)
1248 {
1249     gchar *summary, *short_summary, *title;
1250 
1251     summary = generate_summary_message(ui->run_context);
1252     gtk_label_set_text(ui->summary, summary);
1253     g_free(summary);
1254 
1255     short_summary = generate_short_summary_message(ui->run_context);
1256     title = g_strdup_printf("%s - Cutter", short_summary);
1257     gtk_window_set_title(GTK_WINDOW(ui->window), title);
1258     g_free(short_summary);
1259     g_free(title);
1260 }
1261 
1262 static void
cb_pass_assertion(CutRunContext * run_context,CutTest * test,CutTestContext * test_context,gpointer data)1263 cb_pass_assertion (CutRunContext *run_context,
1264                    CutTest *test, CutTestContext *test_context,
1265                    gpointer data)
1266 {
1267     RowInfo *row_info = data;
1268 
1269     /* slow */
1270     if (g_random_int_range(0, 1000) == 0) {
1271         update_summary(row_info->ui);
1272     }
1273 }
1274 
1275 static void
cb_success_test(CutRunContext * run_context,CutTest * test,CutTestContext * context,CutTestResult * result,gpointer data)1276 cb_success_test (CutRunContext *run_context,
1277                  CutTest *test, CutTestContext *context, CutTestResult *result,
1278                  gpointer data)
1279 {
1280     RowInfo *row_info = data;
1281 
1282     if (row_info->status == -1) {
1283         row_info->status = CUT_TEST_RESULT_SUCCESS;
1284 
1285         update_test_row_status(row_info);
1286     }
1287 }
1288 
1289 static void
cb_failure_test(CutRunContext * run_context,CutTest * test,CutTestContext * context,CutTestResult * result,gpointer data)1290 cb_failure_test (CutRunContext *run_context,
1291                  CutTest *test, CutTestContext *context, CutTestResult *result,
1292                  gpointer data)
1293 {
1294     RowInfo *row_info = data;
1295 
1296     update_status(row_info, CUT_TEST_RESULT_FAILURE);
1297 
1298     update_test_row_status(row_info);
1299     append_test_result_row(row_info, result);
1300 }
1301 
1302 static void
cb_error_test(CutRunContext * run_context,CutTest * test,CutTestContext * context,CutTestResult * result,gpointer data)1303 cb_error_test (CutRunContext *run_context,
1304                CutTest *test, CutTestContext *context, CutTestResult *result,
1305                gpointer data)
1306 {
1307     RowInfo *row_info = data;
1308 
1309     update_status(row_info, CUT_TEST_RESULT_ERROR);
1310 
1311     update_test_row_status(row_info);
1312     append_test_result_row(row_info, result);
1313 }
1314 
1315 static void
cb_pending_test(CutRunContext * run_context,CutTest * test,CutTestContext * context,CutTestResult * result,gpointer data)1316 cb_pending_test (CutRunContext *run_context,
1317                  CutTest *test, CutTestContext *context, CutTestResult *result,
1318                  gpointer data)
1319 {
1320     RowInfo *row_info = data;
1321 
1322     update_status(row_info, CUT_TEST_RESULT_PENDING);
1323 
1324     update_test_row_status(row_info);
1325     append_test_result_row(row_info, result);
1326 }
1327 
1328 static void
cb_notification_test(CutRunContext * run_context,CutTest * test,CutTestContext * context,CutTestResult * result,gpointer data)1329 cb_notification_test (CutRunContext *run_context,
1330                       CutTest *test, CutTestContext *context,
1331                       CutTestResult *result, gpointer data)
1332 {
1333     RowInfo *row_info = data;
1334 
1335     update_status(row_info, CUT_TEST_RESULT_NOTIFICATION);
1336 
1337     update_test_row_status(row_info);
1338     append_test_result_row(row_info, result);
1339 }
1340 
1341 static void
cb_omission_test(CutRunContext * run_context,CutTest * test,CutTestContext * context,CutTestResult * result,gpointer data)1342 cb_omission_test (CutRunContext *run_context,
1343                   CutTest *test, CutTestContext *context,
1344                   CutTestResult *result, gpointer data)
1345 {
1346     RowInfo *row_info = data;
1347 
1348     update_status(row_info, CUT_TEST_RESULT_OMISSION);
1349 
1350     update_test_row_status(row_info);
1351     append_test_result_row(row_info, result);
1352 }
1353 
1354 static void
cb_crash_test(CutRunContext * run_context,CutTest * test,CutTestContext * context,CutTestResult * result,gpointer data)1355 cb_crash_test (CutRunContext *run_context,
1356                CutTest *test, CutTestContext *context,
1357                CutTestResult *result, gpointer data)
1358 {
1359     RowInfo *row_info = data;
1360 
1361     update_status(row_info, CUT_TEST_RESULT_CRASH);
1362 
1363     update_test_row_status(row_info);
1364     append_test_result_row(row_info, result);
1365 }
1366 
1367 static void
increment_n_completed_tests(RowInfo * row_info)1368 increment_n_completed_tests (RowInfo *row_info)
1369 {
1370     CutGtkUI *ui;
1371     RowInfo *parent_row_info;
1372 
1373     ui = row_info->ui;
1374     for (parent_row_info = row_info->parent_row_info;
1375          parent_row_info;
1376          parent_row_info = parent_row_info->parent_row_info) {
1377         parent_row_info->n_completed_tests++;
1378         update_row(ui, parent_row_info);
1379     }
1380 
1381     ui->n_completed_tests++;
1382     update_summary(ui);
1383 }
1384 
1385 static void
cb_complete_test(CutRunContext * run_context,CutTest * test,CutTestContext * test_context,gboolean success,gpointer data)1386 cb_complete_test (CutRunContext *run_context,
1387                   CutTest *test, CutTestContext *test_context,
1388                   gboolean success, gpointer data)
1389 {
1390     RowInfo *row_info;
1391     TestRowInfo *info = data;
1392     CutGtkUI *ui;
1393 
1394     row_info = &(info->row_info);
1395     increment_n_completed_tests(row_info);
1396     ui = row_info->ui;
1397     pop_message(ui, "test");
1398     free_test_row_info(info);
1399 
1400     update_progress_bar(ui);
1401 
1402 #define DISCONNECT(name, user_data)                                     \
1403     g_signal_handlers_disconnect_by_func(run_context,                   \
1404                                          G_CALLBACK(cb_ ## name),       \
1405                                          user_data)
1406     DISCONNECT(pass_assertion, row_info);
1407     DISCONNECT(success_test, row_info);
1408     DISCONNECT(failure_test, row_info);
1409     DISCONNECT(error_test, row_info);
1410     DISCONNECT(pending_test, row_info);
1411     DISCONNECT(notification_test, row_info);
1412     DISCONNECT(omission_test, row_info);
1413     DISCONNECT(crash_test, row_info);
1414     DISCONNECT(complete_test, info);
1415 #undef DISCONNECT
1416 }
1417 
1418 static void
cb_start_test(CutRunContext * run_context,CutTest * test,CutTestContext * test_context,gpointer data)1419 cb_start_test (CutRunContext *run_context,
1420                CutTest *test,
1421                CutTestContext *test_context, gpointer data)
1422 {
1423     RowInfo *row_info;
1424     TestRowInfo *info;
1425     CutGtkUI *ui;
1426 
1427     info = g_new0(TestRowInfo, 1);
1428     info->test_case_row_info = data;
1429     info->test = g_object_ref(test);
1430 
1431     row_info = &(info->row_info);
1432     row_info->parent_row_info = &(info->test_case_row_info->row_info);
1433     ui = row_info->parent_row_info->ui;
1434     row_info->ui = g_object_ref(ui);
1435     row_info->status = -1;
1436     row_info->pulse = 0;
1437     row_info->update_pulse_id = 0;
1438     row_info->path = append_row(ui, info->test_case_row_info->row_info.path,
1439                                 cut_test_get_name(test),
1440                                 cut_test_get_description(test));
1441 
1442     push_message(ui, "test",
1443                  _("Running test: %s"), cut_test_get_name(test));
1444     /* Always expand running test case row. Is it OK? */
1445     expand_row(ui, row_info->path);
1446     row_info->update_pulse_id = g_timeout_add(10, timeout_cb_pulse_test,
1447                                               row_info);
1448 
1449 
1450 #define CONNECT(name, user_data)                                        \
1451     g_signal_connect(run_context, #name, G_CALLBACK(cb_ ## name), user_data)
1452 
1453     CONNECT(pass_assertion, row_info);
1454     CONNECT(success_test, row_info);
1455     CONNECT(failure_test, row_info);
1456     CONNECT(error_test, row_info);
1457     CONNECT(pending_test, row_info);
1458     CONNECT(notification_test, row_info);
1459     CONNECT(omission_test, row_info);
1460     CONNECT(crash_test, row_info);
1461     CONNECT(complete_test, info);
1462 #undef CONNECT
1463 }
1464 
1465 static void
free_iterated_test_row_info(IteratedTestRowInfo * info)1466 free_iterated_test_row_info (IteratedTestRowInfo *info)
1467 {
1468     RowInfo *row_info;
1469 
1470     row_info = &(info->row_info);
1471     disable_progress(row_info);
1472 
1473     g_object_unref(info->iterated_test);
1474     g_free(info->data_name);
1475     g_object_unref(row_info->ui);
1476     g_free(row_info->path);
1477 
1478     g_free(info);
1479 }
1480 
1481 static void
cb_complete_iterated_test(CutRunContext * run_context,CutIteratedTest * iterated_test,CutTestContext * test_context,gboolean success,gpointer data)1482 cb_complete_iterated_test (CutRunContext *run_context,
1483                            CutIteratedTest *iterated_test,
1484                            CutTestContext *test_context,
1485                            gboolean success, gpointer data)
1486 {
1487     RowInfo *row_info;
1488     IteratedTestRowInfo *info = data;
1489     CutGtkUI *ui;
1490 
1491     row_info = &(info->row_info);
1492     ui = row_info->ui;
1493     increment_n_completed_tests(row_info);
1494     pop_message(ui, "iterated-test");
1495     free_iterated_test_row_info(info);
1496 
1497 #define DISCONNECT(name, user_data)                                     \
1498     g_signal_handlers_disconnect_by_func(run_context,                   \
1499                                          G_CALLBACK(cb_ ## name),       \
1500                                          user_data)
1501     DISCONNECT(pass_assertion, row_info);
1502     DISCONNECT(success_test, row_info);
1503     DISCONNECT(failure_test, row_info);
1504     DISCONNECT(error_test, row_info);
1505     DISCONNECT(pending_test, row_info);
1506     DISCONNECT(notification_test, row_info);
1507     DISCONNECT(omission_test, row_info);
1508     DISCONNECT(crash_test, row_info);
1509     DISCONNECT(complete_iterated_test, info);
1510 #undef DISCONNECT
1511 }
1512 
1513 static void
cb_start_iterated_test(CutRunContext * run_context,CutIteratedTest * iterated_test,CutTestContext * test_context,gpointer data)1514 cb_start_iterated_test (CutRunContext *run_context,
1515                         CutIteratedTest *iterated_test,
1516                         CutTestContext *test_context, gpointer data)
1517 {
1518     RowInfo *row_info;
1519     IteratedTestRowInfo *info;
1520     CutGtkUI *ui;
1521 
1522     info = g_new0(IteratedTestRowInfo, 1);
1523     info->test_iterator_row_info = data;
1524     info->iterated_test = g_object_ref(iterated_test);
1525     if (cut_test_context_have_data(test_context)) {
1526         CutTestData *data;
1527         data = cut_test_context_get_current_data(test_context);
1528         info->data_name = g_strdup(cut_test_data_get_name(data));
1529     }
1530     if (!info->data_name) {
1531         info->data_name = g_strdup(cut_test_get_name(CUT_TEST(iterated_test)));
1532     }
1533     row_info = &(info->row_info);
1534     row_info->parent_row_info = &(info->test_iterator_row_info->row_info);
1535     ui = row_info->parent_row_info->ui;
1536     row_info->ui = g_object_ref(ui);
1537     row_info->status = -1;
1538     row_info->pulse = 0;
1539     row_info->update_pulse_id = 0;
1540     row_info->path = append_row(ui,
1541                                 row_info->parent_row_info->path,
1542                                 info->data_name, NULL);
1543 
1544     push_message(ui,
1545                  "iterated-test",
1546                  _("Running iterated test: %s (%s)"),
1547                  cut_test_get_name(CUT_TEST(info->iterated_test)),
1548                  info->data_name);
1549     /* Always expand running test case row. Is it OK? */
1550     expand_row(ui, row_info->path);
1551 
1552 #define CONNECT(name, user_data)                                        \
1553     g_signal_connect(run_context, #name, G_CALLBACK(cb_ ## name), user_data)
1554 
1555     CONNECT(pass_assertion, row_info);
1556     CONNECT(success_test, row_info);
1557     CONNECT(failure_test, row_info);
1558     CONNECT(error_test, row_info);
1559     CONNECT(pending_test, row_info);
1560     CONNECT(notification_test, row_info);
1561     CONNECT(omission_test, row_info);
1562     CONNECT(crash_test, row_info);
1563     CONNECT(complete_iterated_test, info);
1564 #undef CONNECT
1565 }
1566 
1567 static void
collapse_test_iterator_row(TestIteratorRowInfo * info)1568 collapse_test_iterator_row (TestIteratorRowInfo *info)
1569 {
1570     RowInfo *row_info;
1571     CutGtkUI *ui;
1572     GtkTreeIter iter;
1573 
1574     row_info = &(info->row_info);
1575     ui = row_info->ui;
1576 
1577     if (row_info->status == CUT_TEST_RESULT_SUCCESS &&
1578         gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(ui->logs),
1579                                             &iter, row_info->path)) {
1580 
1581         GtkTreePath *path;
1582         path = gtk_tree_model_get_path(GTK_TREE_MODEL(ui->logs), &iter);
1583         gtk_tree_view_collapse_row(ui->tree_view, path);
1584         gtk_tree_path_free(path);
1585     }
1586 }
1587 
1588 static void
free_test_iterator_row_info(TestIteratorRowInfo * info)1589 free_test_iterator_row_info (TestIteratorRowInfo *info)
1590 {
1591     RowInfo *row_info;
1592     CutGtkUI *ui;
1593 
1594     row_info = &(info->row_info);
1595     ui = row_info->ui;
1596 
1597     g_object_unref(info->test_iterator);
1598     g_object_unref(ui);
1599     g_free(row_info->path);
1600 
1601     g_free(info);
1602 }
1603 
1604 static void
cb_complete_test_iterator(CutRunContext * run_context,CutTestCase * test_case,gboolean success,gpointer data)1605 cb_complete_test_iterator (CutRunContext *run_context,
1606                            CutTestCase *test_case, gboolean success,
1607                            gpointer data)
1608 {
1609     RowInfo *row_info;
1610     TestIteratorRowInfo *info = data;
1611 
1612     row_info = &(info->row_info);
1613     update_summary(row_info->ui);
1614     update_row_status(row_info);
1615     collapse_test_iterator_row(info);
1616     free_test_iterator_row_info(info);
1617 
1618     g_signal_handlers_disconnect_by_func(run_context,
1619                                          G_CALLBACK(cb_start_iterated_test),
1620                                          data);
1621     g_signal_handlers_disconnect_by_func(run_context,
1622                                          G_CALLBACK(cb_complete_test_iterator),
1623                                          data);
1624 }
1625 
1626 static void
cb_ready_test_iterator(CutRunContext * run_context,CutTestIterator * test_iterator,guint n_tests,TestCaseRowInfo * test_case_row_info)1627 cb_ready_test_iterator (CutRunContext *run_context,
1628                         CutTestIterator *test_iterator, guint n_tests,
1629                         TestCaseRowInfo *test_case_row_info)
1630 {
1631     RowInfo *row_info;
1632     TestIteratorRowInfo *info;
1633     CutGtkUI *ui;
1634 
1635     info = g_new0(TestIteratorRowInfo, 1);
1636     info->test_case_row_info = test_case_row_info;
1637     info->test_iterator = g_object_ref(test_iterator);
1638     row_info = &(info->row_info);
1639     row_info->parent_row_info = &(test_case_row_info->row_info);
1640     ui = row_info->parent_row_info->ui;
1641     row_info->ui = g_object_ref(ui);
1642     row_info->n_tests = n_tests;
1643     row_info->n_completed_tests = 0;
1644     row_info->status = CUT_TEST_RESULT_SUCCESS;
1645 
1646     ui->n_tests += n_tests - 1;
1647     row_info->parent_row_info->n_tests += n_tests - 1;
1648 
1649     row_info->path = append_row(ui,
1650                                 row_info->parent_row_info->path,
1651                                 cut_test_get_name(CUT_TEST(test_iterator)),
1652                                 cut_test_get_description(CUT_TEST(test_iterator)));
1653 
1654     g_signal_connect(run_context, "start-iterated-test",
1655                      G_CALLBACK(cb_start_iterated_test), info);
1656     g_signal_connect(run_context, "complete-test-iterator",
1657                      G_CALLBACK(cb_complete_test_iterator), info);
1658 }
1659 
1660 static void
collapse_test_case_row(TestCaseRowInfo * info)1661 collapse_test_case_row (TestCaseRowInfo *info)
1662 {
1663     CutGtkUI *ui;
1664     GtkTreeIter iter;
1665 
1666     ui = info->row_info.ui;
1667 
1668     if (info->row_info.status == CUT_TEST_RESULT_SUCCESS &&
1669         gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(ui->logs),
1670                                             &iter, info->row_info.path)) {
1671 
1672         GtkTreePath *path;
1673         path = gtk_tree_model_get_path(GTK_TREE_MODEL(ui->logs), &iter);
1674         gtk_tree_view_collapse_row(ui->tree_view, path);
1675         gtk_tree_path_free(path);
1676     }
1677 }
1678 
1679 static void
cb_complete_test_case(CutRunContext * run_context,CutTestCase * test_case,gboolean success,gpointer data)1680 cb_complete_test_case (CutRunContext *run_context,
1681                        CutTestCase *test_case, gboolean success,
1682                        gpointer data)
1683 {
1684     RowInfo *row_info;
1685     TestCaseRowInfo *info = data;
1686 
1687     row_info = &(info->row_info);
1688     update_summary(row_info->ui);
1689     update_row_status(row_info);
1690     collapse_test_case_row(info);
1691     free_test_case_row_info(info);
1692     g_signal_handlers_disconnect_by_func(run_context,
1693                                          G_CALLBACK(cb_start_test),
1694                                          data);
1695     g_signal_handlers_disconnect_by_func(run_context,
1696                                          G_CALLBACK(cb_ready_test_iterator),
1697                                          data);
1698     g_signal_handlers_disconnect_by_func(run_context,
1699                                          G_CALLBACK(cb_complete_test_case),
1700                                          data);
1701 }
1702 
1703 static void
cb_ready_test_case(CutRunContext * run_context,CutTestCase * test_case,guint n_tests,CutGtkUI * ui)1704 cb_ready_test_case (CutRunContext *run_context, CutTestCase *test_case,
1705                     guint n_tests, CutGtkUI *ui)
1706 {
1707     RowInfo *row_info;
1708     TestCaseRowInfo *info;
1709 
1710     info = g_new0(TestCaseRowInfo, 1);
1711     info->test_case = g_object_ref(test_case);
1712     row_info = &(info->row_info);
1713     row_info->parent_row_info = NULL;
1714     row_info->ui = g_object_ref(ui);
1715     row_info->n_tests = n_tests;
1716     row_info->n_completed_tests = 0;
1717     row_info->status = CUT_TEST_RESULT_SUCCESS;
1718 
1719     row_info->path = append_row(row_info->ui, NULL,
1720                                 cut_test_get_name(CUT_TEST(test_case)),
1721                                 cut_test_get_description(CUT_TEST(test_case)));
1722 
1723     g_signal_connect(run_context, "start-test",
1724                      G_CALLBACK(cb_start_test), info);
1725     g_signal_connect(run_context, "ready-test-iterator",
1726                      G_CALLBACK(cb_ready_test_iterator), info);
1727     g_signal_connect(run_context, "complete-test-case",
1728                      G_CALLBACK(cb_complete_test_case), info);
1729 }
1730 
1731 static void
cb_complete_run(CutRunContext * run_context,gboolean success,CutGtkUI * ui)1732 cb_complete_run (CutRunContext *run_context, gboolean success, CutGtkUI *ui)
1733 {
1734     ui->running = FALSE;
1735     update_button_sensitive(ui);
1736 }
1737 
1738 static void
cb_complete_test_suite(CutRunContext * run_context,CutTestSuite * test_suite,gboolean success,CutGtkUI * ui)1739 cb_complete_test_suite (CutRunContext *run_context,
1740                         CutTestSuite *test_suite,
1741                         gboolean success,
1742                         CutGtkUI *ui)
1743 {
1744     gchar *summary;
1745 
1746     pop_message(ui, "test-suite");
1747 
1748     summary = generate_summary_message(ui->run_context);
1749     push_message(ui, "test-suite",
1750                  _("Finished in %0.1f seconds: %s"),
1751                  cut_run_context_get_elapsed(ui->run_context),
1752                  summary);
1753     g_free(summary);
1754 }
1755 
1756 static void
cb_error(CutRunContext * run_context,GError * error,CutGtkUI * ui)1757 cb_error (CutRunContext *run_context, GError *error, CutGtkUI *ui)
1758 {
1759     g_print("SystemError: %s:%d: %s\n",
1760             g_quark_to_string(error->domain),
1761             error->code,
1762             error->message);
1763 }
1764 
1765 static void
connect_to_run_context(CutGtkUI * ui,CutRunContext * run_context)1766 connect_to_run_context (CutGtkUI *ui, CutRunContext *run_context)
1767 {
1768 #define CONNECT(name) \
1769     g_signal_connect(run_context, #name, G_CALLBACK(cb_ ## name), ui)
1770 
1771     CONNECT(ready_test_suite);
1772     CONNECT(ready_test_case);
1773 
1774     CONNECT(complete_test_suite);
1775     CONNECT(complete_run);
1776 
1777     CONNECT(error);
1778 #undef CONNECT
1779 }
1780 
1781 static void
disconnect_from_run_context(CutGtkUI * ui,CutRunContext * run_context)1782 disconnect_from_run_context (CutGtkUI *ui, CutRunContext *run_context)
1783 {
1784 #define DISCONNECT(name)                                                \
1785     g_signal_handlers_disconnect_by_func(run_context,                   \
1786                                          G_CALLBACK(cb_ ## name),       \
1787                                          ui)
1788 
1789     DISCONNECT(ready_test_suite);
1790     DISCONNECT(ready_test_case);
1791 
1792     DISCONNECT(complete_test_suite);
1793     DISCONNECT(complete_run);
1794 
1795     DISCONNECT(error);
1796 #undef DISCONNECT
1797 }
1798 
1799 static void
attach_to_run_context(CutListener * listener,CutRunContext * run_context)1800 attach_to_run_context (CutListener *listener,
1801                        CutRunContext *run_context)
1802 {
1803     connect_to_run_context(CUT_GTK_UI(listener), run_context);
1804 }
1805 
1806 static void
detach_from_run_context(CutListener * listener,CutRunContext * run_context)1807 detach_from_run_context (CutListener *listener,
1808                          CutRunContext *run_context)
1809 {
1810     disconnect_from_run_context(CUT_GTK_UI(listener), run_context);
1811 }
1812 
1813 static gboolean
run(CutUI * ui,CutRunContext * run_context)1814 run (CutUI *ui, CutRunContext *run_context)
1815 {
1816     CutGtkUI *gtk_ui = CUT_GTK_UI(ui);
1817     CutRunContext *pipeline;
1818 
1819     if (CUT_IS_STREAM_READER(run_context)) {
1820         pipeline = run_context;
1821         g_object_ref(pipeline);
1822     } else {
1823         pipeline = cut_pipeline_new_from_run_context(run_context);
1824     }
1825 
1826     gtk_ui->run_context = pipeline;
1827     gtk_widget_show_all(gtk_ui->window);
1828     gtk_tree_store_clear(gtk_ui->logs);
1829 
1830     run_test(gtk_ui);
1831 
1832     gtk_main();
1833 
1834     return TRUE;
1835 }
1836 
1837 /*
1838 vi:ts=4:nowrap:ai:expandtab:sw=4
1839 */
1840