1 | /* |
2 | * Copyright (C) 2011 Red Hat Inc. |
3 | * |
4 | * Author: |
5 | * Benjamin Otte <otte@gnome.org> |
6 | * |
7 | * This library is free software; you can redistribute it and/or |
8 | * modify it under the terms of the GNU Library General Public |
9 | * License as published by the Free Software Foundation; either |
10 | * version 2 of the License, or (at your option) any later version. |
11 | * |
12 | * This library is distributed in the hope that it will be useful, |
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
15 | * Library General Public License for more details. |
16 | * |
17 | * You should have received a copy of the GNU Library General Public |
18 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
19 | */ |
20 | |
21 | #include "config.h" |
22 | |
23 | #include "reftest-compare.h" |
24 | #include "reftest-module.h" |
25 | #include "reftest-snapshot.h" |
26 | |
27 | #ifndef G_OS_WIN32 |
28 | #include <execinfo.h> |
29 | #endif |
30 | #include <string.h> |
31 | #include <glib/gstdio.h> |
32 | #include <gtk/gtk.h> |
33 | |
34 | #ifdef G_OS_WIN32 |
35 | # include <direct.h> |
36 | #endif |
37 | |
38 | typedef enum { |
39 | SNAPSHOT_WINDOW, |
40 | SNAPSHOT_DRAW |
41 | } SnapshotMode; |
42 | |
43 | /* This is exactly the style information you've been looking for */ |
44 | #define GTK_STYLE_PROVIDER_PRIORITY_FORCE G_MAXUINT |
45 | |
46 | static char *arg_output_dir = NULL; |
47 | static char *arg_base_dir = NULL; |
48 | static char *arg_direction = NULL; |
49 | static char *arg_compare_dir = NULL; |
50 | |
51 | static const GOptionEntry test_args[] = { |
52 | { "output" , 'o', 0, G_OPTION_ARG_FILENAME, &arg_output_dir, |
53 | "Directory to save image files to" , "DIR" }, |
54 | { "directory" , 'd', 0, G_OPTION_ARG_FILENAME, &arg_base_dir, |
55 | "Directory to run tests from" , "DIR" }, |
56 | { "direction" , 0, 0, G_OPTION_ARG_STRING, &arg_direction, |
57 | "Set text direction" , "ltr|rtl" }, |
58 | { "compare-with" , 0, 0, G_OPTION_ARG_FILENAME, &arg_compare_dir, |
59 | "Directory to compare with" , "DIR" }, |
60 | { NULL } |
61 | }; |
62 | |
63 | static gboolean using_tap; |
64 | |
65 | static gboolean |
66 | parse_command_line (int *argc, char ***argv) |
67 | { |
68 | GError *error = NULL; |
69 | GOptionContext *context; |
70 | int i; |
71 | |
72 | context = g_option_context_new (parameter_string: "- run GTK reftests" ); |
73 | g_option_context_add_main_entries (context, entries: test_args, NULL); |
74 | g_option_context_set_ignore_unknown_options (context, TRUE); |
75 | |
76 | if (!g_option_context_parse (context, argc, argv, error: &error)) |
77 | { |
78 | g_print (format: "option parsing failed: %s\n" , error->message); |
79 | return FALSE; |
80 | } |
81 | g_option_context_free (context); |
82 | |
83 | for (i = 0; i < *argc; i++) |
84 | { |
85 | if (strcmp (s1: (*argv)[i], s2: "--tap" ) == 0) |
86 | using_tap = TRUE; |
87 | } |
88 | |
89 | gtk_test_init (argcp: argc, argvp: argv); |
90 | |
91 | if (g_strcmp0 (str1: arg_direction, str2: "rtl" ) == 0) |
92 | gtk_widget_set_default_direction (dir: GTK_TEXT_DIR_RTL); |
93 | else if (g_strcmp0 (str1: arg_direction, str2: "ltr" ) == 0) |
94 | gtk_widget_set_default_direction (dir: GTK_TEXT_DIR_LTR); |
95 | else if (arg_direction != NULL) |
96 | g_printerr (format: "Invalid argument passed to --direction argument. Valid arguments are 'ltr' and 'rtl'\n" ); |
97 | |
98 | return TRUE; |
99 | } |
100 | |
101 | static const char * |
102 | get_output_dir (GError **error) |
103 | { |
104 | static const char *output_dir = NULL; |
105 | |
106 | if (output_dir) |
107 | return output_dir; |
108 | |
109 | if (arg_output_dir) |
110 | { |
111 | GError *err = NULL; |
112 | GFile *file; |
113 | const char *subdir; |
114 | |
115 | file = g_file_new_for_commandline_arg (arg: arg_output_dir); |
116 | |
117 | subdir = g_getenv (variable: "TEST_OUTPUT_SUBDIR" ); |
118 | if (subdir) |
119 | { |
120 | GFile *child = g_file_get_child (file, name: subdir); |
121 | g_object_unref (object: file); |
122 | file = child; |
123 | } |
124 | |
125 | if (!g_file_make_directory_with_parents (file, NULL, error: &err)) |
126 | { |
127 | if (!g_error_matches (error: err, G_IO_ERROR, code: G_IO_ERROR_EXISTS)) |
128 | { |
129 | g_propagate_error (dest: error, src: err); |
130 | g_object_unref (object: file); |
131 | return NULL; |
132 | } |
133 | g_clear_error (err: &err); |
134 | } |
135 | |
136 | output_dir = g_file_get_path (file); |
137 | g_object_unref (object: file); |
138 | } |
139 | else |
140 | { |
141 | output_dir = g_get_tmp_dir (); |
142 | } |
143 | |
144 | return output_dir; |
145 | } |
146 | |
147 | static void |
148 | get_components_of_test_file (const char *test_file, |
149 | char **directory, |
150 | char **basename) |
151 | { |
152 | if (directory) |
153 | { |
154 | *directory = g_path_get_dirname (file_name: test_file); |
155 | } |
156 | |
157 | if (basename) |
158 | { |
159 | char *base = g_path_get_basename (file_name: test_file); |
160 | |
161 | if (g_str_has_suffix (str: base, suffix: ".ui" )) |
162 | base[strlen (s: base) - strlen (s: ".ui" )] = '\0'; |
163 | |
164 | *basename = base; |
165 | } |
166 | } |
167 | |
168 | static char * |
169 | get_output_file (const char *test_file, |
170 | const char *extension, |
171 | GError **error) |
172 | { |
173 | const char *output_dir; |
174 | char *result, *base; |
175 | |
176 | output_dir = get_output_dir (error); |
177 | if (output_dir == NULL) |
178 | return NULL; |
179 | |
180 | get_components_of_test_file (test_file, NULL, basename: &base); |
181 | |
182 | result = g_strconcat (string1: output_dir, G_DIR_SEPARATOR_S, base, extension, NULL); |
183 | g_free (mem: base); |
184 | |
185 | return result; |
186 | } |
187 | |
188 | static char * |
189 | get_test_file (const char *test_file, |
190 | const char *extension, |
191 | gboolean must_exist) |
192 | { |
193 | GString *file = g_string_new (NULL); |
194 | char *dir, *base; |
195 | |
196 | get_components_of_test_file (test_file, directory: &dir, basename: &base); |
197 | |
198 | g_string_append (string: file, val: dir); |
199 | g_string_append (string: file, G_DIR_SEPARATOR_S); |
200 | g_string_append (string: file, val: base); |
201 | g_string_append (string: file, val: extension); |
202 | |
203 | g_free (mem: dir); |
204 | g_free (mem: base); |
205 | |
206 | if (must_exist && |
207 | !g_file_test (filename: file->str, test: G_FILE_TEST_EXISTS)) |
208 | { |
209 | g_string_free (string: file, TRUE); |
210 | return NULL; |
211 | } |
212 | |
213 | return g_string_free (string: file, FALSE); |
214 | } |
215 | |
216 | static char * |
217 | get_reference_image (const char *ui_file) |
218 | { |
219 | char *base; |
220 | char *reference_image; |
221 | |
222 | if (!arg_compare_dir) |
223 | return NULL; |
224 | |
225 | get_components_of_test_file (test_file: ui_file, NULL, basename: &base); |
226 | reference_image = g_strconcat (string1: arg_compare_dir, G_DIR_SEPARATOR_S, base, ".out.png" , NULL); |
227 | g_free (mem: base); |
228 | |
229 | if (!g_file_test (filename: reference_image, test: G_FILE_TEST_EXISTS)) |
230 | { |
231 | g_free (mem: reference_image); |
232 | return NULL; |
233 | } |
234 | |
235 | return reference_image; |
236 | } |
237 | |
238 | static GtkStyleProvider * |
239 | (const char *testname, |
240 | const char *extension) |
241 | { |
242 | GtkStyleProvider *provider = NULL; |
243 | char *css_file; |
244 | |
245 | css_file = get_test_file (test_file: testname, extension, TRUE); |
246 | if (css_file == NULL) |
247 | return NULL; |
248 | |
249 | provider = GTK_STYLE_PROVIDER (gtk_css_provider_new ()); |
250 | gtk_css_provider_load_from_path (GTK_CSS_PROVIDER (provider), |
251 | path: css_file); |
252 | gtk_style_context_add_provider_for_display (display: gdk_display_get_default (), |
253 | provider, |
254 | GTK_STYLE_PROVIDER_PRIORITY_FORCE); |
255 | |
256 | g_free (mem: css_file); |
257 | |
258 | return provider; |
259 | } |
260 | |
261 | static void |
262 | (GtkStyleProvider *provider) |
263 | { |
264 | if (provider == NULL) |
265 | return; |
266 | |
267 | gtk_style_context_remove_provider_for_display (display: gdk_display_get_default (), |
268 | provider); |
269 | } |
270 | |
271 | static void |
272 | save_image (GdkTexture *texture, |
273 | const char *test_name, |
274 | const char *extension) |
275 | { |
276 | GError *error = NULL; |
277 | char *filename; |
278 | gboolean ret; |
279 | |
280 | filename = get_output_file (test_file: test_name, extension, error: &error); |
281 | if (filename == NULL) |
282 | { |
283 | g_test_message (format: "Not storing test result image: %s" , error->message); |
284 | g_error_free (error); |
285 | return; |
286 | } |
287 | |
288 | g_test_message (format: "Storing test result image at %s" , filename); |
289 | ret = gdk_texture_save_to_png (texture, filename); |
290 | g_assert_true (ret); |
291 | |
292 | g_free (mem: filename); |
293 | } |
294 | |
295 | static void |
296 | save_node (GskRenderNode *node, |
297 | const char *test_name, |
298 | const char *extension) |
299 | { |
300 | GError *error = NULL; |
301 | char *filename; |
302 | gboolean ret; |
303 | GBytes *bytes; |
304 | |
305 | filename = get_output_file (test_file: test_name, extension, error: &error); |
306 | if (filename == NULL) |
307 | { |
308 | g_test_message (format: "Not storing test result node: %s" , error->message); |
309 | g_error_free (error); |
310 | return; |
311 | } |
312 | |
313 | g_test_message (format: "Storing test result node at %s" , filename); |
314 | if (node) |
315 | bytes = gsk_render_node_serialize (node); |
316 | else |
317 | bytes = g_bytes_new (data: "" , size: 0); |
318 | ret = g_file_set_contents (filename, |
319 | contents: g_bytes_get_data (bytes, NULL), |
320 | length: g_bytes_get_size (bytes), |
321 | NULL); |
322 | g_assert_true (ret); |
323 | |
324 | g_bytes_unref (bytes); |
325 | g_free (mem: filename); |
326 | } |
327 | |
328 | static void |
329 | test_ui_file (GFile *file) |
330 | { |
331 | char *ui_file, *reference_file; |
332 | GdkTexture *ui_image, *reference_image, *diff_image; |
333 | GtkStyleProvider *provider; |
334 | |
335 | ui_file = g_file_get_path (file); |
336 | |
337 | provider = add_extra_css (testname: ui_file, extension: ".css" ); |
338 | |
339 | ui_image = reftest_snapshot_ui_file (ui_file); |
340 | |
341 | if ((reference_file = get_reference_image (ui_file)) != NULL) |
342 | { |
343 | GError *error = NULL; |
344 | |
345 | reference_image = gdk_texture_new_from_filename (path: reference_file, error: &error); |
346 | if (reference_image == NULL) |
347 | { |
348 | g_test_message (format: "Failed to load reference image: %s" , error->message); |
349 | g_clear_error (err: &error); |
350 | g_test_fail (); |
351 | } |
352 | } |
353 | else if ((reference_file = get_test_file (test_file: ui_file, extension: ".ref.ui" , TRUE)) != NULL) |
354 | reference_image = reftest_snapshot_ui_file (ui_file: reference_file); |
355 | else |
356 | { |
357 | reference_image = NULL; |
358 | g_test_message (format: "No reference image." ); |
359 | g_test_fail (); |
360 | } |
361 | g_free (mem: reference_file); |
362 | if (reference_image == NULL) |
363 | reference_image = gdk_memory_texture_new (width: 1, height: 1, GDK_MEMORY_DEFAULT, bytes: g_bytes_new (data: (guchar[4]) {0, 0, 0, 0}, size: 4), stride: 4); |
364 | |
365 | diff_image = reftest_compare_textures (texture1: ui_image, texture2: reference_image); |
366 | |
367 | save_image (texture: ui_image, test_name: ui_file, extension: ".out.png" ); |
368 | save_image (texture: reference_image, test_name: ui_file, extension: ".ref.png" ); |
369 | if (diff_image) |
370 | { |
371 | save_node (node: g_object_get_data (G_OBJECT (ui_image), key: "source-render-node" ), test_name: ui_file, extension: ".out.node" ); |
372 | save_node (node: g_object_get_data (G_OBJECT (reference_image), key: "source-render-node" ), test_name: ui_file, extension: ".ref.node" ); |
373 | save_image (texture: diff_image, test_name: ui_file, extension: ".diff.png" ); |
374 | g_object_unref (object: diff_image); |
375 | g_test_fail (); |
376 | } |
377 | |
378 | remove_extra_css (provider); |
379 | |
380 | g_free (mem: ui_file); |
381 | |
382 | g_clear_object (&ui_image); |
383 | g_clear_object (&reference_image); |
384 | } |
385 | |
386 | static int |
387 | compare_files (gconstpointer a, gconstpointer b) |
388 | { |
389 | GFile *file1 = G_FILE (a); |
390 | GFile *file2 = G_FILE (b); |
391 | char *path1, *path2; |
392 | int result; |
393 | |
394 | path1 = g_file_get_path (file: file1); |
395 | path2 = g_file_get_path (file: file2); |
396 | |
397 | result = strcmp (s1: path1, s2: path2); |
398 | |
399 | g_free (mem: path1); |
400 | g_free (mem: path2); |
401 | |
402 | return result; |
403 | } |
404 | |
405 | static void |
406 | add_test_for_file (GFile *file) |
407 | { |
408 | GFileEnumerator *enumerator; |
409 | GFileInfo *info; |
410 | GList *files; |
411 | GError *error = NULL; |
412 | |
413 | |
414 | if (g_file_query_file_type (file, flags: 0, NULL) != G_FILE_TYPE_DIRECTORY) |
415 | { |
416 | g_test_add_vtable (testpath: g_file_peek_path (file), |
417 | data_size: 0, |
418 | g_object_ref (file), |
419 | NULL, |
420 | data_test: (GTestFixtureFunc) test_ui_file, |
421 | data_teardown: (GTestFixtureFunc) g_object_unref); |
422 | return; |
423 | } |
424 | |
425 | |
426 | enumerator = g_file_enumerate_children (file, G_FILE_ATTRIBUTE_STANDARD_NAME, flags: 0, NULL, error: &error); |
427 | g_assert_no_error (error); |
428 | files = NULL; |
429 | |
430 | while ((info = g_file_enumerator_next_file (enumerator, NULL, error: &error))) |
431 | { |
432 | const char *filename; |
433 | |
434 | filename = g_file_info_get_name (info); |
435 | |
436 | if (!g_str_has_suffix (str: filename, suffix: ".ui" ) || |
437 | g_str_has_suffix (str: filename, suffix: ".ref.ui" )) |
438 | { |
439 | g_object_unref (object: info); |
440 | continue; |
441 | } |
442 | |
443 | files = g_list_prepend (list: files, data: g_file_get_child (file, name: filename)); |
444 | |
445 | g_object_unref (object: info); |
446 | } |
447 | |
448 | g_assert_no_error (error); |
449 | g_object_unref (object: enumerator); |
450 | |
451 | files = g_list_sort (list: files, compare_func: compare_files); |
452 | g_list_foreach (list: files, func: (GFunc) add_test_for_file, NULL); |
453 | g_list_free_full (list: files, free_func: g_object_unref); |
454 | } |
455 | |
456 | static GLogWriterOutput |
457 | log_writer (GLogLevelFlags log_level, |
458 | const GLogField *fields, |
459 | gsize n_fields, |
460 | gpointer user_data) |
461 | { |
462 | #ifndef G_OS_WIN32 |
463 | if (log_level & G_LOG_LEVEL_CRITICAL) |
464 | { |
465 | void *buffer[1024]; |
466 | int size, i; |
467 | char **symbols; |
468 | GString *s; |
469 | GLogField *my_fields; |
470 | |
471 | my_fields = g_alloca (sizeof (GLogField) * n_fields); |
472 | |
473 | s = g_string_new (init: "" ); |
474 | |
475 | size = backtrace (array: buffer, size: 1024); |
476 | symbols = backtrace_symbols (array: buffer, size: size); |
477 | for (i = 0; i < size; i++) |
478 | { |
479 | g_string_append (string: s, val: symbols[i]); |
480 | g_string_append_c (s, '\n'); |
481 | } |
482 | free (ptr: symbols); |
483 | |
484 | for (i = 0; i < n_fields; i++) |
485 | { |
486 | my_fields[i] = fields[i]; |
487 | |
488 | if (strcmp (s1: fields[i].key, s2: "MESSAGE" ) == 0) |
489 | { |
490 | my_fields[i].value = g_strconcat (string1: fields[i].value, "\nBacktrace:\n" , s->str, NULL); |
491 | my_fields[i].length = strlen (s: my_fields[i].value); |
492 | } |
493 | } |
494 | g_string_free (string: s, TRUE); |
495 | |
496 | fields = my_fields; |
497 | } |
498 | #endif |
499 | |
500 | if (!g_log_writer_default_would_drop (log_level, NULL)) |
501 | return g_log_writer_standard_streams (log_level, fields, n_fields, user_data); |
502 | |
503 | return G_LOG_WRITER_HANDLED; |
504 | } |
505 | |
506 | int |
507 | main (int argc, char **argv) |
508 | { |
509 | const char *basedir; |
510 | int result; |
511 | |
512 | if (!parse_command_line (argc: &argc, argv: &argv)) |
513 | return 1; |
514 | |
515 | /* Override some settings that otherwise might affect |
516 | * the reliability of our output. |
517 | */ |
518 | g_object_set (object: gtk_settings_get_default (), |
519 | first_property_name: "gtk-cursor-blink" , FALSE, |
520 | NULL); |
521 | |
522 | if (arg_base_dir) |
523 | basedir = arg_base_dir; |
524 | else |
525 | basedir = g_test_get_dir (file_type: G_TEST_DIST); |
526 | |
527 | if (argc < 2) |
528 | { |
529 | GFile *dir; |
530 | |
531 | dir = g_file_new_for_path (path: basedir); |
532 | |
533 | add_test_for_file (file: dir); |
534 | |
535 | g_object_unref (object: dir); |
536 | } |
537 | else |
538 | { |
539 | guint i; |
540 | |
541 | for (i = 1; i < argc; i++) |
542 | { |
543 | GFile *file = g_file_new_for_commandline_arg (arg: argv[i]); |
544 | |
545 | add_test_for_file (file); |
546 | |
547 | g_object_unref (object: file); |
548 | } |
549 | } |
550 | |
551 | /* We need to ensure the process' current working directory |
552 | * is the same as the reftest data, because we're using the |
553 | * "file" property of GtkImage as a relative path in builder files. |
554 | * |
555 | * The g_assert() is needed to ensure GNU libc does not complain |
556 | * about the unused return value, and the G_GNUC_UNUSED is needed |
557 | * to avoid compiler warnings when g_assert() is compiled out |
558 | * during the release build. |
559 | */ |
560 | int res G_GNUC_UNUSED; |
561 | res = chdir (path: basedir); |
562 | g_assert (res == 0); |
563 | |
564 | g_log_set_writer_func (func: log_writer, NULL, NULL); |
565 | |
566 | result = g_test_run (); |
567 | |
568 | if (using_tap) |
569 | return 0; |
570 | |
571 | return result; |
572 | } |
573 | |
574 | |