#include "config.h" #include #include #include static gboolean skip_win32 (void) { #ifdef G_OS_WIN32 g_test_skip ("FIXME, test is broken on win32"); return TRUE; #else return FALSE; #endif } /* These tests were written for the inotify implementation. * Other implementations may require slight adjustments in * the tests, e.g. the length of timeouts */ typedef struct { GFile *tmp_dir; } Fixture; static void setup (Fixture *fixture, gconstpointer user_data) { gchar *path = NULL; GError *local_error = NULL; path = g_dir_make_tmp ("gio-test-testfilemonitor_XXXXXX", &local_error); g_assert_no_error (local_error); fixture->tmp_dir = g_file_new_for_path (path); g_test_message ("Using temporary directory: %s", path); g_free (path); } static void teardown (Fixture *fixture, gconstpointer user_data) { GError *local_error = NULL; g_file_delete (fixture->tmp_dir, NULL, &local_error); g_assert_no_error (local_error); g_clear_object (&fixture->tmp_dir); } typedef enum { NONE = 0, INOTIFY = (1 << 1), KQUEUE = (1 << 2) } Environment; typedef struct { gint event_type; gchar *file; gchar *other_file; gint step; /* Since different file monitor implementation has different capabilities, * we cannot expect all implementations to report all kind of events without * any loss. This 'optional' field is a bit mask used to mark events which * may be lost under specific platforms. */ Environment optional; } RecordedEvent; static void free_recorded_event (RecordedEvent *event) { g_free (event->file); g_free (event->other_file); g_free (event); } typedef struct { GFile *file; GFileMonitor *monitor; GMainLoop *loop; gint step; GList *events; GFileOutputStream *output_stream; } TestData; static void output_event (const RecordedEvent *event) { if (event->step >= 0) g_test_message (">>>> step %d", event->step); else { GTypeClass *class; class = g_type_class_ref (g_type_from_name ("GFileMonitorEvent")); g_test_message ("%s file=%s other_file=%s\n", g_enum_get_value (G_ENUM_CLASS (class), event->event_type)->value_nick, event->file, event->other_file); g_type_class_unref (class); } } /* a placeholder for temp file names we don't want to compare */ static const gchar DONT_CARE[] = ""; static Environment get_environment (GFileMonitor *monitor) { if (g_str_equal (G_OBJECT_TYPE_NAME (monitor), "GInotifyFileMonitor")) return INOTIFY; if (g_str_equal (G_OBJECT_TYPE_NAME (monitor), "GKqueueFileMonitor")) return KQUEUE; return NONE; } static void check_expected_events (RecordedEvent *expected, gsize n_expected, GList *recorded, Environment env) { gsize i; gint li; GList *l; for (i = 0, li = 0, l = recorded; i < n_expected && l != NULL;) { RecordedEvent *e1 = &expected[i]; RecordedEvent *e2 = l->data; gboolean mismatch = TRUE; gboolean l_extra_step = FALSE; do { gboolean ignore_other_file = FALSE; if (e1->step != e2->step) break; /* Kqueue isn't good at detecting file renaming, so * G_FILE_MONITOR_WATCH_MOVES is mostly useless there. */ if (e1->event_type != e2->event_type && env & KQUEUE) { /* It is possible for kqueue file monitor to emit 'RENAMED' event, * but most of the time it is reported as a 'DELETED' event and * a 'CREATED' event. */ if (e1->event_type == G_FILE_MONITOR_EVENT_RENAMED) { RecordedEvent *e2_next; if (l->next == NULL) break; e2_next = l->next->data; if (e2->event_type != G_FILE_MONITOR_EVENT_DELETED) break; if (e2_next->event_type != G_FILE_MONITOR_EVENT_CREATED) break; if (e1->step != e2_next->step) break; if (e1->file != DONT_CARE && (g_strcmp0 (e1->file, e2->file) != 0 || e2->other_file != NULL)) break; if (e1->other_file != DONT_CARE && (g_strcmp0 (e1->other_file, e2_next->file) != 0 || e2_next->other_file != NULL)) break; l_extra_step = TRUE; mismatch = FALSE; break; } /* Kqueue won't report 'MOVED_IN' and 'MOVED_OUT' events. We set * 'ignore_other_file' here to let the following code know that * 'other_file' may not match. */ else if (e1->event_type == G_FILE_MONITOR_EVENT_MOVED_IN) { if (e2->event_type != G_FILE_MONITOR_EVENT_CREATED) break; ignore_other_file = TRUE; } else if (e1->event_type == G_FILE_MONITOR_EVENT_MOVED_OUT) { if (e2->event_type != G_FILE_MONITOR_EVENT_DELETED) break; ignore_other_file = TRUE; } else break; } if (e1->file != DONT_CARE && g_strcmp0 (e1->file, e2->file) != 0) break; if (e1->other_file != DONT_CARE && !ignore_other_file && g_strcmp0 (e1->other_file, e2->other_file) != 0) break; mismatch = FALSE; } while (0); if (mismatch) { /* Sometimes the emission of 'CHANGES_DONE_HINT' may be late because * it depends on the ability of file monitor implementation to report * 'CHANGES_DONE_HINT' itself. If the file monitor implementation * doesn't report 'CHANGES_DONE_HINT' itself, it may be emitted by * GLocalFileMonitor after a few seconds, which causes the event to * mix with results from different steps. Since 'CHANGES_DONE_HINT' * is just a hint, we don't require it to be reliable and we simply * ignore unexpected 'CHANGES_DONE_HINT' events here. */ if (e1->event_type != G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT && e2->event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) { g_test_message ("Event CHANGES_DONE_HINT ignored at " "expected index %" G_GSIZE_FORMAT ", recorded index %d", i, li); li++, l = l->next; continue; } /* If an event is marked as optional in the current environment and * the event doesn't match, it means the expected event has lost. */ else if (env & e1->optional) { g_test_message ("Event %d at expected index %" G_GSIZE_FORMAT " skipped because " "it is marked as optional", e1->event_type, i); i++; continue; } /* Run above checks under g_assert_* again to provide more useful * error messages. Print the expected and actual events first. */ else { GList *ll; gsize j; g_test_message ("Recorded events:"); for (ll = recorded; ll != NULL; ll = ll->next) output_event ((RecordedEvent *) ll->data); g_test_message ("Expected events:"); for (j = 0; j < n_expected; j++) output_event (&expected[j]); g_assert_cmpint (e1->step, ==, e2->step); g_assert_cmpint (e1->event_type, ==, e2->event_type); if (e1->file != DONT_CARE) g_assert_cmpstr (e1->file, ==, e2->file); if (e1->other_file != DONT_CARE) g_assert_cmpstr (e1->other_file, ==, e2->other_file); g_assert_not_reached (); } } i++, li++, l = l->next; if (l_extra_step) li++, l = l->next; } g_assert_cmpint (i, ==, n_expected); g_assert_cmpint (li, ==, g_list_length (recorded)); } static void record_event (TestData *data, gint event_type, const gchar *file, const gchar *other_file, gint step) { RecordedEvent *event; event = g_new0 (RecordedEvent, 1); event->event_type = event_type; event->file = g_strdup (file); event->other_file = g_strdup (other_file); event->step = step; data->events = g_list_append (data->events, event); } static void monitor_changed (GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { TestData *data = user_data; gchar *basename, *other_base; basename = g_file_get_basename (file); if (other_file) other_base = g_file_get_basename (other_file); else other_base = NULL; record_event (data, event_type, basename, other_base, -1); g_free (basename); g_free (other_base); } static gboolean atomic_replace_step (gpointer user_data) { TestData *data = user_data; GError *error = NULL; switch (data->step) { case 0: record_event (data, -1, NULL, NULL, 0); g_file_replace_contents (data->file, "step 0", 6, NULL, FALSE, G_FILE_CREATE_NONE, NULL, NULL, &error); g_assert_no_error (error); break; case 1: record_event (data, -1, NULL, NULL, 1); g_file_replace_contents (data->file, "step 1", 6, NULL, FALSE, G_FILE_CREATE_NONE, NULL, NULL, &error); g_assert_no_error (error); break; case 2: record_event (data, -1, NULL, NULL, 2); g_file_delete (data->file, NULL, NULL); break; case 3: record_event (data, -1, NULL, NULL, 3); g_main_loop_quit (data->loop); return G_SOURCE_REMOVE; } data->step++; return G_SOURCE_CONTINUE; } /* this is the output we expect from the above steps */ static RecordedEvent atomic_replace_output[] = { { -1, NULL, NULL, 0, NONE }, { G_FILE_MONITOR_EVENT_CREATED, "atomic_replace_file", NULL, -1, NONE }, { G_FILE_MONITOR_EVENT_CHANGED, "atomic_replace_file", NULL, -1, KQUEUE }, { G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "atomic_replace_file", NULL, -1, KQUEUE }, { -1, NULL, NULL, 1, NONE }, { G_FILE_MONITOR_EVENT_RENAMED, (gchar*)DONT_CARE, "atomic_replace_file", -1, NONE }, { -1, NULL, NULL, 2, NONE }, { G_FILE_MONITOR_EVENT_DELETED, "atomic_replace_file", NULL, -1, NONE }, { -1, NULL, NULL, 3, NONE } }; static void test_atomic_replace (Fixture *fixture, gconstpointer user_data) { GError *error = NULL; TestData data; if (skip_win32 ()) return; data.step = 0; data.events = NULL; data.file = g_file_get_child (fixture->tmp_dir, "atomic_replace_file"); g_file_delete (data.file, NULL, NULL); data.monitor = g_file_monitor_file (data.file, G_FILE_MONITOR_WATCH_MOVES, NULL, &error); g_assert_no_error (error); g_test_message ("Using GFileMonitor %s", G_OBJECT_TYPE_NAME (data.monitor)); g_file_monitor_set_rate_limit (data.monitor, 200); g_signal_connect (data.monitor, "changed", G_CALLBACK (monitor_changed), &data); data.loop = g_main_loop_new (NULL, TRUE); g_timeout_add (500, atomic_replace_step, &data); g_main_loop_run (data.loop); check_expected_events (atomic_replace_output, G_N_ELEMENTS (atomic_replace_output), data.events, get_environment (data.monitor)); g_list_free_full (data.events, (GDestroyNotify)free_recorded_event); g_main_loop_unref (data.loop); g_object_unref (data.monitor); g_object_unref (data.file); } static gboolean change_step (gpointer user_data) { TestData *data = user_data; GOutputStream *stream; GError *error = NULL; guint32 mode = 0660; switch (data->step) { case 0: record_event (data, -1, NULL, NULL, 0); g_file_replace_contents (data->file, "step 0", 6, NULL, FALSE, G_FILE_CREATE_NONE, NULL, NULL, &error); g_assert_no_error (error); break; case 1: record_event (data, -1, NULL, NULL, 1); stream = (GOutputStream *)g_file_append_to (data->file, G_FILE_CREATE_NONE, NULL, &error); g_assert_no_error (error); g_output_stream_write_all (stream, " step 1", 7, NULL, NULL, &error); g_assert_no_error (error); g_output_stream_close (stream, NULL, &error); g_assert_no_error (error); g_object_unref (stream); break; case 2: record_event (data, -1, NULL, NULL, 2); g_file_set_attribute (data->file, G_FILE_ATTRIBUTE_UNIX_MODE, G_FILE_ATTRIBUTE_TYPE_UINT32, &mode, G_FILE_QUERY_INFO_NONE, NULL, &error); g_assert_no_error (error); break; case 3: record_event (data, -1, NULL, NULL, 3); g_file_delete (data->file, NULL, NULL); break; case 4: record_event (data, -1, NULL, NULL, 4); g_main_loop_quit (data->loop); return G_SOURCE_REMOVE; } data->step++; return G_SOURCE_CONTINUE; } /* this is the output we expect from the above steps */ static RecordedEvent change_output[] = { { -1, NULL, NULL, 0, NONE }, { G_FILE_MONITOR_EVENT_CREATED, "change_file", NULL, -1, NONE }, { G_FILE_MONITOR_EVENT_CHANGED, "change_file", NULL, -1, KQUEUE }, { G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "change_file", NULL, -1, KQUEUE }, { -1, NULL, NULL, 1, NONE }, { G_FILE_MONITOR_EVENT_CHANGED, "change_file", NULL, -1, NONE }, { G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "change_file", NULL, -1, NONE }, { -1, NULL, NULL, 2, NONE }, { G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED, "change_file", NULL, -1, NONE }, { -1, NULL, NULL, 3, NONE }, { G_FILE_MONITOR_EVENT_DELETED, "change_file", NULL, -1, NONE }, { -1, NULL, NULL, 4, NONE } }; static void test_file_changes (Fixture *fixture, gconstpointer user_data) { GError *error = NULL; TestData data; if (skip_win32 ()) return; data.step = 0; data.events = NULL; data.file = g_file_get_child (fixture->tmp_dir, "change_file"); g_file_delete (data.file, NULL, NULL); data.monitor = g_file_monitor_file (data.file, G_FILE_MONITOR_WATCH_MOVES, NULL, &error); g_assert_no_error (error); g_test_message ("Using GFileMonitor %s", G_OBJECT_TYPE_NAME (data.monitor)); g_file_monitor_set_rate_limit (data.monitor, 200); g_signal_connect (data.monitor, "changed", G_CALLBACK (monitor_changed), &data); data.loop = g_main_loop_new (NULL, TRUE); g_timeout_add (500, change_step, &data); g_main_loop_run (data.loop); check_expected_events (change_output, G_N_ELEMENTS (change_output), data.events, get_environment (data.monitor)); g_list_free_full (data.events, (GDestroyNotify)free_recorded_event); g_main_loop_unref (data.loop); g_object_unref (data.monitor); g_object_unref (data.file); } static gboolean dir_step (gpointer user_data) { TestData *data = user_data; GFile *parent, *file, *file2; GError *error = NULL; switch (data->step) { case 1: record_event (data, -1, NULL, NULL, 1); parent = g_file_get_parent (data->file); file = g_file_get_child (parent, "dir_test_file"); g_file_replace_contents (file, "step 1", 6, NULL, FALSE, G_FILE_CREATE_NONE, NULL, NULL, &error); g_assert_no_error (error); g_object_unref (file); g_object_unref (parent); break; case 2: record_event (data, -1, NULL, NULL, 2); parent = g_file_get_parent (data->file); file = g_file_get_child (parent, "dir_test_file"); file2 = g_file_get_child (data->file, "dir_test_file"); g_file_move (file, file2, G_FILE_COPY_NONE, NULL, NULL, NULL, &error); g_assert_no_error (error); g_object_unref (file); g_object_unref (file2); g_object_unref (parent); break; case 3: record_event (data, -1, NULL, NULL, 3); file = g_file_get_child (data->file, "dir_test_file"); file2 = g_file_get_child (data->file, "dir_test_file2"); g_file_move (file, file2, G_FILE_COPY_NONE, NULL, NULL, NULL, &error); g_assert_no_error (error); g_object_unref (file); g_object_unref (file2); break; case 4: record_event (data, -1, NULL, NULL, 4); parent = g_file_get_parent (data->file); file = g_file_get_child (data->file, "dir_test_file2"); file2 = g_file_get_child (parent, "dir_test_file2"); g_file_move (file, file2, G_FILE_COPY_NONE, NULL, NULL, NULL, &error); g_assert_no_error (error); g_file_delete (file2, NULL, NULL); g_object_unref (file); g_object_unref (file2); g_object_unref (parent); break; case 5: record_event (data, -1, NULL, NULL, 5); g_file_delete (data->file, NULL, NULL); break; case 6: record_event (data, -1, NULL, NULL, 6); g_main_loop_quit (data->loop); return G_SOURCE_REMOVE; } data->step++; return G_SOURCE_CONTINUE; } /* this is the output we expect from the above steps */ static RecordedEvent dir_output[] = { { -1, NULL, NULL, 1, NONE }, { -1, NULL, NULL, 2, NONE }, { G_FILE_MONITOR_EVENT_MOVED_IN, "dir_test_file", NULL, -1, NONE }, { -1, NULL, NULL, 3, NONE }, { G_FILE_MONITOR_EVENT_RENAMED, "dir_test_file", "dir_test_file2", -1, NONE }, { -1, NULL, NULL, 4, NONE }, { G_FILE_MONITOR_EVENT_MOVED_OUT, "dir_test_file2", NULL, -1, NONE }, { -1, NULL, NULL, 5, NONE }, { G_FILE_MONITOR_EVENT_DELETED, "dir_monitor_test", NULL, -1, NONE }, { -1, NULL, NULL, 6, NONE } }; static void test_dir_monitor (Fixture *fixture, gconstpointer user_data) { GError *error = NULL; TestData data; if (skip_win32 ()) return; data.step = 0; data.events = NULL; data.file = g_file_get_child (fixture->tmp_dir, "dir_monitor_test"); g_file_delete (data.file, NULL, NULL); g_file_make_directory (data.file, NULL, &error); data.monitor = g_file_monitor_directory (data.file, G_FILE_MONITOR_WATCH_MOVES, NULL, &error); g_assert_no_error (error); g_test_message ("Using GFileMonitor %s", G_OBJECT_TYPE_NAME (data.monitor)); g_file_monitor_set_rate_limit (data.monitor, 200); g_signal_connect (data.monitor, "changed", G_CALLBACK (monitor_changed), &data); data.loop = g_main_loop_new (NULL, TRUE); g_timeout_add (500, dir_step, &data); g_main_loop_run (data.loop); check_expected_events (dir_output, G_N_ELEMENTS (dir_output), data.events, get_environment (data.monitor)); g_list_free_full (data.events, (GDestroyNotify)free_recorded_event); g_main_loop_unref (data.loop); g_object_unref (data.monitor); g_object_unref (data.file); } static gboolean nodir_step (gpointer user_data) { TestData *data = user_data; GFile *parent; GError *error = NULL; switch (data->step) { case 0: record_event (data, -1, NULL, NULL, 0); parent = g_file_get_parent (data->file); g_file_make_directory (parent, NULL, &error); g_assert_no_error (error); g_object_unref (parent); break; case 1: record_event (data, -1, NULL, NULL, 1); g_file_replace_contents (data->file, "step 1", 6, NULL, FALSE, G_FILE_CREATE_NONE, NULL, NULL, &error); g_assert_no_error (error); break; case 2: record_event (data, -1, NULL, NULL, 2); g_file_delete (data->file, NULL, &error); g_assert_no_error (error); break; case 3: record_event (data, -1, NULL, NULL, 3); parent = g_file_get_parent (data->file); g_file_delete (parent, NULL, &error); g_assert_no_error (error); g_object_unref (parent); break; case 4: record_event (data, -1, NULL, NULL, 4); g_main_loop_quit (data->loop); return G_SOURCE_REMOVE; } data->step++; return G_SOURCE_CONTINUE; } static RecordedEvent nodir_output[] = { { -1, NULL, NULL, 0, NONE }, { G_FILE_MONITOR_EVENT_CREATED, "nosuchfile", NULL, -1, KQUEUE }, { G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "nosuchfile", NULL, -1, KQUEUE }, { -1, NULL, NULL, 1, NONE }, { G_FILE_MONITOR_EVENT_CREATED, "nosuchfile", NULL, -1, NONE }, { G_FILE_MONITOR_EVENT_CHANGED, "nosuchfile", NULL, -1, KQUEUE }, { G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "nosuchfile", NULL, -1, KQUEUE }, { -1, NULL, NULL, 2, NONE }, { G_FILE_MONITOR_EVENT_DELETED, "nosuchfile", NULL, -1, NONE }, { -1, NULL, NULL, 3, NONE }, { -1, NULL, NULL, 4, NONE } }; static void test_dir_non_existent (Fixture *fixture, gconstpointer user_data) { TestData data; GError *error = NULL; if (skip_win32 ()) return; data.step = 0; data.events = NULL; data.file = g_file_get_child (fixture->tmp_dir, "nosuchdir/nosuchfile"); data.monitor = g_file_monitor_file (data.file, G_FILE_MONITOR_WATCH_MOVES, NULL, &error); g_assert_no_error (error); g_test_message ("Using GFileMonitor %s", G_OBJECT_TYPE_NAME (data.monitor)); g_file_monitor_set_rate_limit (data.monitor, 200); g_signal_connect (data.monitor, "changed", G_CALLBACK (monitor_changed), &data); data.loop = g_main_loop_new (NULL, TRUE); /* we need a long timeout here, since the inotify implementation only scans * for missing files every 4 seconds. */ g_timeout_add (5000, nodir_step, &data); g_main_loop_run (data.loop); check_expected_events (nodir_output, G_N_ELEMENTS (nodir_output), data.events, get_environment (data.monitor)); g_list_free_full (data.events, (GDestroyNotify)free_recorded_event); g_main_loop_unref (data.loop); g_object_unref (data.monitor); g_object_unref (data.file); } static gboolean cross_dir_step (gpointer user_data) { TestData *data = user_data; GFile *file, *file2; GError *error = NULL; switch (data[0].step) { case 0: record_event (&data[0], -1, NULL, NULL, 0); record_event (&data[1], -1, NULL, NULL, 0); file = g_file_get_child (data[1].file, "a"); g_file_replace_contents (file, "step 0", 6, NULL, FALSE, G_FILE_CREATE_NONE, NULL, NULL, &error); g_assert_no_error (error); g_object_unref (file); break; case 1: record_event (&data[0], -1, NULL, NULL, 1); record_event (&data[1], -1, NULL, NULL, 1); file = g_file_get_child (data[1].file, "a"); file2 = g_file_get_child (data[0].file, "a"); g_file_move (file, file2, 0, NULL, NULL, NULL, &error); g_assert_no_error (error); g_object_unref (file); g_object_unref (file2); break; case 2: record_event (&data[0], -1, NULL, NULL, 2); record_event (&data[1], -1, NULL, NULL, 2); file2 = g_file_get_child (data[0].file, "a"); g_file_delete (file2, NULL, NULL); g_file_delete (data[0].file, NULL, NULL); g_file_delete (data[1].file, NULL, NULL); g_object_unref (file2); break; case 3: record_event (&data[0], -1, NULL, NULL, 3); record_event (&data[1], -1, NULL, NULL, 3); g_main_loop_quit (data->loop); return G_SOURCE_REMOVE; } data->step++; return G_SOURCE_CONTINUE; } static RecordedEvent cross_dir_a_output[] = { { -1, NULL, NULL, 0, NONE }, { -1, NULL, NULL, 1, NONE }, { G_FILE_MONITOR_EVENT_CREATED, "a", NULL, -1, NONE }, { G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "a", NULL, -1, KQUEUE }, { -1, NULL, NULL, 2, NONE }, { G_FILE_MONITOR_EVENT_DELETED, "a", NULL, -1, NONE }, { G_FILE_MONITOR_EVENT_DELETED, "cross_dir_a", NULL, -1, NONE }, { -1, NULL, NULL, 3, NONE }, }; static RecordedEvent cross_dir_b_output[] = { { -1, NULL, NULL, 0, NONE }, { G_FILE_MONITOR_EVENT_CREATED, "a", NULL, -1, NONE }, { G_FILE_MONITOR_EVENT_CHANGED, "a", NULL, -1, KQUEUE }, { G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "a", NULL, -1, KQUEUE }, { -1, NULL, NULL, 1, NONE }, { G_FILE_MONITOR_EVENT_MOVED_OUT, "a", "a", -1, NONE }, { -1, NULL, NULL, 2, NONE }, { G_FILE_MONITOR_EVENT_DELETED, "cross_dir_b", NULL, -1, NONE }, { -1, NULL, NULL, 3, NONE }, }; static void test_cross_dir_moves (Fixture *fixture, gconstpointer user_data) { GError *error = NULL; TestData data[2]; if (skip_win32 ()) return; data[0].step = 0; data[0].events = NULL; data[0].file = g_file_get_child (fixture->tmp_dir, "cross_dir_a"); g_file_delete (data[0].file, NULL, NULL); g_file_make_directory (data[0].file, NULL, &error); data[0].monitor = g_file_monitor_directory (data[0].file, 0, NULL, &error); g_assert_no_error (error); g_test_message ("Using GFileMonitor 0 %s", G_OBJECT_TYPE_NAME (data[0].monitor)); g_file_monitor_set_rate_limit (data[0].monitor, 200); g_signal_connect (data[0].monitor, "changed", G_CALLBACK (monitor_changed), &data[0]); data[1].step = 0; data[1].events = NULL; data[1].file = g_file_get_child (fixture->tmp_dir, "cross_dir_b"); g_file_delete (data[1].file, NULL, NULL); g_file_make_directory (data[1].file, NULL, &error); data[1].monitor = g_file_monitor_directory (data[1].file, G_FILE_MONITOR_WATCH_MOVES, NULL, &error); g_assert_no_error (error); g_test_message ("Using GFileMonitor 1 %s", G_OBJECT_TYPE_NAME (data[1].monitor)); g_file_monitor_set_rate_limit (data[1].monitor, 200); g_signal_connect (data[1].monitor, "changed", G_CALLBACK (monitor_changed), &data[1]); data[0].loop = g_main_loop_new (NULL, TRUE); g_timeout_add (500, cross_dir_step, data); g_main_loop_run (data[0].loop); check_expected_events (cross_dir_a_output, G_N_ELEMENTS (cross_dir_a_output), data[0].events, get_environment (data[0].monitor)); check_expected_events (cross_dir_b_output, G_N_ELEMENTS (cross_dir_b_output), data[1].events, get_environment (data[1].monitor)); g_list_free_full (data[0].events, (GDestroyNotify)free_recorded_event); g_main_loop_unref (data[0].loop); g_object_unref (data[0].monitor); g_object_unref (data[0].file); g_list_free_full (data[1].events, (GDestroyNotify)free_recorded_event); g_object_unref (data[1].monitor); g_object_unref (data[1].file); } static gboolean file_hard_links_step (gpointer user_data) { gboolean retval = G_SOURCE_CONTINUE; TestData *data = user_data; GError *error = NULL; gchar *filename = g_file_get_path (data->file); gchar *hard_link_name = g_strdup_printf ("%s2", filename); GFile *hard_link_file = g_file_new_for_path (hard_link_name); switch (data->step) { case 0: record_event (data, -1, NULL, NULL, 0); g_output_stream_write_all (G_OUTPUT_STREAM (data->output_stream), "hello, step 0", 13, NULL, NULL, &error); g_assert_no_error (error); g_output_stream_close (G_OUTPUT_STREAM (data->output_stream), NULL, &error); g_assert_no_error (error); break; case 1: record_event (data, -1, NULL, NULL, 1); g_file_replace_contents (data->file, "step 1", 6, NULL, FALSE, G_FILE_CREATE_NONE, NULL, NULL, &error); g_assert_no_error (error); break; case 2: record_event (data, -1, NULL, NULL, 2); #ifdef HAVE_LINK if (link (filename, hard_link_name) < 0) { g_error ("link(%s, %s) failed: %s", filename, hard_link_name, g_strerror (errno)); } #endif /* HAVE_LINK */ break; case 3: record_event (data, -1, NULL, NULL, 3); #ifdef HAVE_LINK { GOutputStream *hard_link_stream = NULL; /* Deliberately don’t do an atomic swap on the hard-linked file. */ hard_link_stream = G_OUTPUT_STREAM (g_file_append_to (hard_link_file, G_FILE_CREATE_NONE, NULL, &error)); g_assert_no_error (error); g_output_stream_write_all (hard_link_stream, " step 3", 7, NULL, NULL, &error); g_assert_no_error (error); g_output_stream_close (hard_link_stream, NULL, &error); g_assert_no_error (error); g_object_unref (hard_link_stream); } #endif /* HAVE_LINK */ break; case 4: record_event (data, -1, NULL, NULL, 4); g_file_delete (data->file, NULL, &error); g_assert_no_error (error); break; case 5: record_event (data, -1, NULL, NULL, 5); #ifdef HAVE_LINK g_file_delete (hard_link_file, NULL, &error); g_assert_no_error (error); #endif /* HAVE_LINK */ break; case 6: record_event (data, -1, NULL, NULL, 6); g_main_loop_quit (data->loop); retval = G_SOURCE_REMOVE; break; } if (retval != G_SOURCE_REMOVE) data->step++; g_object_unref (hard_link_file); g_free (hard_link_name); g_free (filename); return retval; } static RecordedEvent file_hard_links_output[] = { { -1, NULL, NULL, 0, NONE }, { G_FILE_MONITOR_EVENT_CHANGED, "testfilemonitor.db", NULL, -1, NONE }, { G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT, "testfilemonitor.db", NULL, -1, NONE }, { -1, NULL, NULL, 1, NONE }, { G_FILE_MONITOR_EVENT_RENAMED, (gchar*)DONT_CARE /* .goutputstream-XXXXXX */, "testfilemonitor.db", -1, NONE }, { -1, NULL, NULL, 2, NONE }, { -1, NULL, NULL, 3, NONE }, /* Kqueue is based on file descriptors. You can get events from all hard * links by just monitoring one open file descriptor, and it is not possible * to know whether it is done on the file name we use to open the file. Since * the hard link count of 'testfilemonitor.db' is 2, it is expected to see * two 'DELETED' events reported here. You have to call 'unlink' twice on * different file names to remove 'testfilemonitor.db' from the file system, * and each 'unlink' call generates a 'DELETED' event. */ { G_FILE_MONITOR_EVENT_CHANGED, "testfilemonitor.db", NULL, -1, INOTIFY }, { -1, NULL, NULL, 4, NONE }, { G_FILE_MONITOR_EVENT_DELETED, "testfilemonitor.db", NULL, -1, NONE }, { -1, NULL, NULL, 5, NONE }, { G_FILE_MONITOR_EVENT_DELETED, "testfilemonitor.db", NULL, -1, INOTIFY }, { -1, NULL, NULL, 6, NONE }, }; static void test_file_hard_links (Fixture *fixture, gconstpointer user_data) { GError *error = NULL; TestData data; g_test_bug ("https://bugzilla.gnome.org/show_bug.cgi?id=755721"); if (skip_win32 ()) return; #ifdef HAVE_LINK g_test_message ("Running with hard link tests"); #else /* if !HAVE_LINK */ g_test_message ("Running without hard link tests"); #endif /* !HAVE_LINK */ data.step = 0; data.events = NULL; /* Create a file which exists and is not a directory. */ data.file = g_file_get_child (fixture->tmp_dir, "testfilemonitor.db"); data.output_stream = g_file_replace (data.file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, &error); g_assert_no_error (error); /* Monitor it. Creating the monitor should not crash (bug #755721). */ data.monitor = g_file_monitor_file (data.file, G_FILE_MONITOR_WATCH_MOUNTS | G_FILE_MONITOR_WATCH_MOVES | G_FILE_MONITOR_WATCH_HARD_LINKS, NULL, &error); g_assert_no_error (error); g_assert_nonnull (data.monitor); g_test_message ("Using GFileMonitor %s", G_OBJECT_TYPE_NAME (data.monitor)); /* Change the file a bit. */ g_file_monitor_set_rate_limit (data.monitor, 200); g_signal_connect (data.monitor, "changed", (GCallback) monitor_changed, &data); data.loop = g_main_loop_new (NULL, TRUE); g_timeout_add (500, file_hard_links_step, &data); g_main_loop_run (data.loop); check_expected_events (file_hard_links_output, G_N_ELEMENTS (file_hard_links_output), data.events, get_environment (data.monitor)); g_list_free_full (data.events, (GDestroyNotify) free_recorded_event); g_main_loop_unref (data.loop); g_object_unref (data.monitor); g_object_unref (data.file); g_object_unref (data.output_stream); } static void test_finalize_in_callback (Fixture *fixture, gconstpointer user_data) { GFile *file = NULL; guint i; g_test_summary ("Test that finalization of a GFileMonitor in one of its " "callbacks doesn’t cause a deadlock."); g_test_bug ("https://gitlab.gnome.org/GNOME/glib/-/issues/1941"); file = g_file_get_child (fixture->tmp_dir, "race-file"); for (i = 0; i < 50; i++) { GFileMonitor *monitor = NULL; GError *local_error = NULL; /* Monitor the file. */ monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, NULL, &local_error); g_assert_no_error (local_error); g_assert_nonnull (monitor); /* Create the file. */ g_file_replace_contents (file, "hello", 5, NULL, FALSE, G_FILE_CREATE_NONE, NULL, NULL, &local_error); g_assert_no_error (local_error); /* Immediately drop the last ref to the monitor in the hope that this * happens in the middle of the critical section in * g_file_monitor_source_handle_event(), so that any cleanup at the end * of that function is done with a now-finalised file monitor. */ g_object_unref (monitor); /* Re-create the monitor and do the same again for deleting the file, to * give a second chance at hitting the race condition. */ monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, NULL, &local_error); g_assert_no_error (local_error); g_assert_nonnull (monitor); /* Delete the file. */ g_file_delete (file, NULL, &local_error); g_assert_no_error (local_error); /* Drop the ref again. */ g_object_unref (monitor); } g_object_unref (file); } static void test_root (Fixture *fixture, gconstpointer user_data) { GFile *file = NULL; GFileMonitor *monitor = NULL; GError *local_error = NULL; g_test_summary ("Test that GFileMonitor can monitor the root directory."); g_test_bug ("https://gitlab.gnome.org/GNOME/glib/-/merge_requests/3241"); #if defined(G_OS_UNIX) file = g_file_new_for_path ("/"); #elif defined(G_OS_WIN32) file = g_file_new_for_path ("C:\\"); #else g_test_skip ("Unsupported root directory"); return; #endif /* We can’t test for any monitor events, but we can at least check that this * doesn’t crash or error. */ monitor = g_file_monitor_directory (file, G_FILE_MONITOR_NONE, NULL, &local_error); g_assert_no_error (local_error); g_assert_nonnull (monitor); g_clear_object (&monitor); g_clear_object (&file); } int main (int argc, char *argv[]) { g_test_init (&argc, &argv, NULL); g_test_add ("/monitor/atomic-replace", Fixture, NULL, setup, test_atomic_replace, teardown); g_test_add ("/monitor/file-changes", Fixture, NULL, setup, test_file_changes, teardown); g_test_add ("/monitor/dir-monitor", Fixture, NULL, setup, test_dir_monitor, teardown); g_test_add ("/monitor/dir-not-existent", Fixture, NULL, setup, test_dir_non_existent, teardown); g_test_add ("/monitor/cross-dir-moves", Fixture, NULL, setup, test_cross_dir_moves, teardown); g_test_add ("/monitor/file/hard-links", Fixture, NULL, setup, test_file_hard_links, teardown); g_test_add ("/monitor/finalize-in-callback", Fixture, NULL, setup, test_finalize_in_callback, teardown); g_test_add ("/monitor/root", Fixture, NULL, setup, test_root, teardown); return g_test_run (); }