| /* |
| * Tests for util/filemonitor-*.c |
| * |
| * Copyright 2018 Red Hat, Inc. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this library; if not, see <http://www.gnu.org/licenses/>. |
| * |
| */ |
| |
| #include "qemu/osdep.h" |
| #include "qemu/main-loop.h" |
| #include "qapi/error.h" |
| #include "qemu/filemonitor.h" |
| |
| #include <utime.h> |
| |
| enum { |
| QFILE_MONITOR_TEST_OP_CREATE, |
| QFILE_MONITOR_TEST_OP_APPEND, |
| QFILE_MONITOR_TEST_OP_TRUNC, |
| QFILE_MONITOR_TEST_OP_RENAME, |
| QFILE_MONITOR_TEST_OP_TOUCH, |
| QFILE_MONITOR_TEST_OP_UNLINK, |
| }; |
| |
| typedef struct { |
| int type; |
| const char *filesrc; |
| const char *filedst; |
| } QFileMonitorTestOp; |
| |
| typedef struct { |
| const char *file; |
| } QFileMonitorTestWatch; |
| |
| typedef struct { |
| gsize nwatches; |
| const QFileMonitorTestWatch *watches; |
| |
| gsize nops; |
| const QFileMonitorTestOp *ops; |
| } QFileMonitorTestPlan; |
| |
| typedef struct { |
| int id; |
| QFileMonitorEvent event; |
| char *filename; |
| } QFileMonitorTestRecord; |
| |
| |
| typedef struct { |
| QemuMutex lock; |
| GList *records; |
| } QFileMonitorTestData; |
| |
| static QemuMutex evlock; |
| static bool evstopping; |
| static bool evrunning; |
| |
| /* |
| * Main function for a background thread that is |
| * running the event loop during the test |
| */ |
| static void * |
| qemu_file_monitor_test_event_loop(void *opaque G_GNUC_UNUSED) |
| { |
| qemu_mutex_lock(&evlock); |
| |
| while (!evstopping) { |
| qemu_mutex_unlock(&evlock); |
| main_loop_wait(true); |
| qemu_mutex_lock(&evlock); |
| } |
| |
| evrunning = false; |
| qemu_mutex_unlock(&evlock); |
| return NULL; |
| } |
| |
| |
| /* |
| * File monitor event handler which simply maintains |
| * an ordered list of all events that it receives |
| */ |
| static void |
| qemu_file_monitor_test_handler(int id, |
| QFileMonitorEvent event, |
| const char *filename, |
| void *opaque) |
| { |
| QFileMonitorTestData *data = opaque; |
| QFileMonitorTestRecord *rec = g_new0(QFileMonitorTestRecord, 1); |
| |
| rec->id = id; |
| rec->event = event; |
| rec->filename = g_strdup(filename); |
| |
| qemu_mutex_lock(&data->lock); |
| data->records = g_list_append(data->records, rec); |
| qemu_mutex_unlock(&data->lock); |
| } |
| |
| |
| static void |
| qemu_file_monitor_test_record_free(QFileMonitorTestRecord *rec) |
| { |
| g_free(rec->filename); |
| g_free(rec); |
| } |
| |
| |
| /* |
| * Get the next event record that has been received by |
| * the file monitor event handler. Since events are |
| * emitted in the background thread running the event |
| * loop, we can't assume there is a record available |
| * immediately. Thus we will sleep for upto 5 seconds |
| * to wait for the event to be queued for us. |
| */ |
| static QFileMonitorTestRecord * |
| qemu_file_monitor_test_next_record(QFileMonitorTestData *data) |
| { |
| GTimer *timer = g_timer_new(); |
| QFileMonitorTestRecord *record = NULL; |
| GList *tmp; |
| |
| qemu_mutex_lock(&data->lock); |
| while (!data->records && g_timer_elapsed(timer, NULL) < 5) { |
| qemu_mutex_unlock(&data->lock); |
| usleep(10 * 1000); |
| qemu_mutex_lock(&data->lock); |
| } |
| if (data->records) { |
| record = data->records->data; |
| tmp = data->records; |
| data->records = g_list_remove_link(data->records, tmp); |
| g_list_free(tmp); |
| } |
| qemu_mutex_unlock(&data->lock); |
| |
| g_timer_destroy(timer); |
| return record; |
| } |
| |
| |
| /* |
| * Check whether the event record we retrieved matches |
| * data we were expecting to see for the event |
| */ |
| static bool |
| qemu_file_monitor_test_expect(QFileMonitorTestData *data, |
| int id, |
| QFileMonitorEvent event, |
| const char *filename) |
| { |
| QFileMonitorTestRecord *rec; |
| bool ret = false; |
| |
| rec = qemu_file_monitor_test_next_record(data); |
| |
| if (!rec) { |
| g_printerr("Missing event watch id %d event %d file %s\n", |
| id, event, filename); |
| return false; |
| } |
| |
| if (id != rec->id) { |
| g_printerr("Expected watch id %d but got %d\n", id, rec->id); |
| goto cleanup; |
| } |
| |
| if (event != rec->event) { |
| g_printerr("Expected event %d but got %d\n", event, rec->event); |
| goto cleanup; |
| } |
| |
| if (!g_str_equal(filename, rec->filename)) { |
| g_printerr("Expected filename %s but got %s\n", |
| filename, rec->filename); |
| goto cleanup; |
| } |
| |
| ret = true; |
| |
| cleanup: |
| qemu_file_monitor_test_record_free(rec); |
| return ret; |
| } |
| |
| |
| static void |
| test_file_monitor_events(const void *opaque) |
| { |
| const QFileMonitorTestPlan *plan = opaque; |
| Error *local_err = NULL; |
| GError *gerr = NULL; |
| QFileMonitor *mon = qemu_file_monitor_new(&local_err); |
| QemuThread th; |
| GTimer *timer; |
| gchar *dir = NULL; |
| int err = -1; |
| gsize i, j; |
| char *pathsrc = NULL; |
| char *pathdst = NULL; |
| QFileMonitorTestData data; |
| |
| qemu_mutex_init(&data.lock); |
| data.records = NULL; |
| |
| /* |
| * The file monitor needs the main loop running in |
| * order to receive events from inotify. We must |
| * thus spawn a background thread to run an event |
| * loop impl, while this thread triggers the |
| * actual file operations we're testing |
| */ |
| evrunning = 1; |
| evstopping = 0; |
| qemu_thread_create(&th, "event-loop", |
| qemu_file_monitor_test_event_loop, NULL, |
| QEMU_THREAD_JOINABLE); |
| |
| if (local_err) { |
| g_printerr("File monitoring not available: %s", |
| error_get_pretty(local_err)); |
| error_free(local_err); |
| return; |
| } |
| |
| dir = g_dir_make_tmp("test-util-filemonitor-XXXXXX", |
| &gerr); |
| if (!dir) { |
| g_printerr("Unable to create tmp dir %s", |
| gerr->message); |
| g_error_free(gerr); |
| abort(); |
| } |
| |
| /* |
| * First register all the directory / file watches |
| * we're interested in seeing events against |
| */ |
| for (i = 0; i < plan->nwatches; i++) { |
| int watchid; |
| watchid = qemu_file_monitor_add_watch(mon, |
| dir, |
| plan->watches[i].file, |
| qemu_file_monitor_test_handler, |
| &data, |
| &local_err); |
| if (watchid < 0) { |
| g_printerr("Unable to add watch %s", |
| error_get_pretty(local_err)); |
| goto cleanup; |
| } |
| } |
| |
| |
| /* |
| * Now invoke all the file operations (create, |
| * delete, rename, chmod, etc). These operations |
| * will trigger the various file monitor events |
| */ |
| for (i = 0; i < plan->nops; i++) { |
| const QFileMonitorTestOp *op = &(plan->ops[i]); |
| int fd; |
| struct utimbuf ubuf; |
| |
| pathsrc = g_strdup_printf("%s/%s", dir, op->filesrc); |
| if (op->filedst) { |
| pathdst = g_strdup_printf("%s/%s", dir, op->filedst); |
| } |
| |
| switch (op->type) { |
| case QFILE_MONITOR_TEST_OP_CREATE: |
| fd = open(pathsrc, O_WRONLY | O_CREAT, 0700); |
| if (fd < 0) { |
| g_printerr("Unable to create %s: %s", |
| pathsrc, strerror(errno)); |
| goto cleanup; |
| } |
| close(fd); |
| break; |
| |
| case QFILE_MONITOR_TEST_OP_APPEND: |
| fd = open(pathsrc, O_WRONLY | O_APPEND, 0700); |
| if (fd < 0) { |
| g_printerr("Unable to open %s: %s", |
| pathsrc, strerror(errno)); |
| goto cleanup; |
| } |
| |
| if (write(fd, "Hello World", 10) != 10) { |
| g_printerr("Unable to write %s: %s", |
| pathsrc, strerror(errno)); |
| close(fd); |
| goto cleanup; |
| } |
| close(fd); |
| break; |
| |
| case QFILE_MONITOR_TEST_OP_TRUNC: |
| if (truncate(pathsrc, 4) < 0) { |
| g_printerr("Unable to truncate %s: %s", |
| pathsrc, strerror(errno)); |
| goto cleanup; |
| } |
| break; |
| |
| case QFILE_MONITOR_TEST_OP_RENAME: |
| if (rename(pathsrc, pathdst) < 0) { |
| g_printerr("Unable to rename %s to %s: %s", |
| pathsrc, pathdst, strerror(errno)); |
| goto cleanup; |
| } |
| break; |
| |
| case QFILE_MONITOR_TEST_OP_UNLINK: |
| if (unlink(pathsrc) < 0) { |
| g_printerr("Unable to unlink %s: %s", |
| pathsrc, strerror(errno)); |
| goto cleanup; |
| } |
| break; |
| |
| case QFILE_MONITOR_TEST_OP_TOUCH: |
| ubuf.actime = 1024; |
| ubuf.modtime = 1025; |
| if (utime(pathsrc, &ubuf) < 0) { |
| g_printerr("Unable to touch %s: %s", |
| pathsrc, strerror(errno)); |
| goto cleanup; |
| } |
| break; |
| |
| default: |
| g_assert_not_reached(); |
| } |
| |
| g_free(pathsrc); |
| g_free(pathdst); |
| pathsrc = pathdst = NULL; |
| } |
| |
| |
| /* |
| * Finally validate that we have received all the events |
| * we expect to see for the combination of watches and |
| * file operations |
| */ |
| for (i = 0; i < plan->nops; i++) { |
| const QFileMonitorTestOp *op = &(plan->ops[i]); |
| |
| switch (op->type) { |
| case QFILE_MONITOR_TEST_OP_CREATE: |
| for (j = 0; j < plan->nwatches; j++) { |
| if (plan->watches[j].file && |
| !g_str_equal(plan->watches[j].file, op->filesrc)) |
| continue; |
| |
| if (!qemu_file_monitor_test_expect( |
| &data, j, QFILE_MONITOR_EVENT_CREATED, op->filesrc)) |
| goto cleanup; |
| } |
| break; |
| |
| case QFILE_MONITOR_TEST_OP_APPEND: |
| case QFILE_MONITOR_TEST_OP_TRUNC: |
| for (j = 0; j < plan->nwatches; j++) { |
| if (plan->watches[j].file && |
| !g_str_equal(plan->watches[j].file, op->filesrc)) |
| continue; |
| |
| if (!qemu_file_monitor_test_expect( |
| &data, j, QFILE_MONITOR_EVENT_MODIFIED, op->filesrc)) |
| goto cleanup; |
| } |
| break; |
| |
| case QFILE_MONITOR_TEST_OP_RENAME: |
| for (j = 0; j < plan->nwatches; j++) { |
| if (plan->watches[j].file && |
| !g_str_equal(plan->watches[j].file, op->filesrc)) |
| continue; |
| |
| if (!qemu_file_monitor_test_expect( |
| &data, j, QFILE_MONITOR_EVENT_DELETED, op->filesrc)) |
| goto cleanup; |
| } |
| |
| for (j = 0; j < plan->nwatches; j++) { |
| if (plan->watches[j].file && |
| !g_str_equal(plan->watches[j].file, op->filedst)) |
| continue; |
| |
| if (!qemu_file_monitor_test_expect( |
| &data, j, QFILE_MONITOR_EVENT_CREATED, op->filedst)) |
| goto cleanup; |
| } |
| break; |
| |
| case QFILE_MONITOR_TEST_OP_TOUCH: |
| for (j = 0; j < plan->nwatches; j++) { |
| if (plan->watches[j].file && |
| !g_str_equal(plan->watches[j].file, op->filesrc)) |
| continue; |
| |
| if (!qemu_file_monitor_test_expect( |
| &data, j, QFILE_MONITOR_EVENT_ATTRIBUTES, op->filesrc)) |
| goto cleanup; |
| } |
| break; |
| |
| case QFILE_MONITOR_TEST_OP_UNLINK: |
| for (j = 0; j < plan->nwatches; j++) { |
| if (plan->watches[j].file && |
| !g_str_equal(plan->watches[j].file, op->filesrc)) |
| continue; |
| |
| if (!qemu_file_monitor_test_expect( |
| &data, j, QFILE_MONITOR_EVENT_DELETED, op->filesrc)) |
| goto cleanup; |
| } |
| break; |
| |
| default: |
| g_assert_not_reached(); |
| } |
| } |
| |
| err = 0; |
| |
| cleanup: |
| g_free(pathsrc); |
| g_free(pathdst); |
| |
| qemu_mutex_lock(&evlock); |
| evstopping = 1; |
| timer = g_timer_new(); |
| while (evrunning && g_timer_elapsed(timer, NULL) < 5) { |
| qemu_mutex_unlock(&evlock); |
| usleep(10 * 1000); |
| qemu_mutex_lock(&evlock); |
| } |
| qemu_mutex_unlock(&evlock); |
| |
| if (g_timer_elapsed(timer, NULL) >= 5) { |
| g_printerr("Event loop failed to quit after 5 seconds\n"); |
| } |
| g_timer_destroy(timer); |
| |
| for (i = 0; i < plan->nops; i++) { |
| const QFileMonitorTestOp *op = &(plan->ops[i]); |
| pathsrc = g_strdup_printf("%s/%s", dir, op->filesrc); |
| unlink(pathsrc); |
| g_free(pathsrc); |
| if (op->filedst) { |
| pathdst = g_strdup_printf("%s/%s", dir, op->filedst); |
| unlink(pathdst); |
| g_free(pathdst); |
| } |
| } |
| |
| qemu_file_monitor_free(mon); |
| g_list_foreach(data.records, |
| (GFunc)qemu_file_monitor_test_record_free, NULL); |
| g_list_free(data.records); |
| qemu_mutex_destroy(&data.lock); |
| if (dir) { |
| rmdir(dir); |
| } |
| g_free(dir); |
| g_assert(err == 0); |
| } |
| |
| |
| /* |
| * Set of structs which define which file name patterns |
| * we're trying to watch against. NULL, means all files |
| * in the directory |
| */ |
| static const QFileMonitorTestWatch watches_any[] = { |
| { NULL }, |
| }; |
| |
| static const QFileMonitorTestWatch watches_one[] = { |
| { "one.txt" }, |
| }; |
| |
| static const QFileMonitorTestWatch watches_two[] = { |
| { "two.txt" }, |
| }; |
| |
| static const QFileMonitorTestWatch watches_many[] = { |
| { NULL }, |
| { "one.txt" }, |
| { "two.txt" }, |
| }; |
| |
| |
| /* |
| * Various sets of file operations we're going to |
| * trigger and validate events for |
| */ |
| static const QFileMonitorTestOp ops_create_one[] = { |
| { .type = QFILE_MONITOR_TEST_OP_CREATE, |
| .filesrc = "one.txt", } |
| }; |
| |
| static const QFileMonitorTestOp ops_delete_one[] = { |
| { .type = QFILE_MONITOR_TEST_OP_CREATE, |
| .filesrc = "one.txt", }, |
| { .type = QFILE_MONITOR_TEST_OP_UNLINK, |
| .filesrc = "one.txt", } |
| }; |
| |
| static const QFileMonitorTestOp ops_create_many[] = { |
| { .type = QFILE_MONITOR_TEST_OP_CREATE, |
| .filesrc = "one.txt", }, |
| { .type = QFILE_MONITOR_TEST_OP_CREATE, |
| .filesrc = "two.txt", }, |
| { .type = QFILE_MONITOR_TEST_OP_CREATE, |
| .filesrc = "three.txt", } |
| }; |
| |
| static const QFileMonitorTestOp ops_rename_one[] = { |
| { .type = QFILE_MONITOR_TEST_OP_CREATE, |
| .filesrc = "one.txt", }, |
| { .type = QFILE_MONITOR_TEST_OP_RENAME, |
| .filesrc = "one.txt", .filedst = "two.txt" } |
| }; |
| |
| static const QFileMonitorTestOp ops_rename_many[] = { |
| { .type = QFILE_MONITOR_TEST_OP_CREATE, |
| .filesrc = "one.txt", }, |
| { .type = QFILE_MONITOR_TEST_OP_CREATE, |
| .filesrc = "two.txt", }, |
| { .type = QFILE_MONITOR_TEST_OP_RENAME, |
| .filesrc = "one.txt", .filedst = "two.txt" } |
| }; |
| |
| static const QFileMonitorTestOp ops_append_one[] = { |
| { .type = QFILE_MONITOR_TEST_OP_CREATE, |
| .filesrc = "one.txt", }, |
| { .type = QFILE_MONITOR_TEST_OP_APPEND, |
| .filesrc = "one.txt", }, |
| }; |
| |
| static const QFileMonitorTestOp ops_trunc_one[] = { |
| { .type = QFILE_MONITOR_TEST_OP_CREATE, |
| .filesrc = "one.txt", }, |
| { .type = QFILE_MONITOR_TEST_OP_TRUNC, |
| .filesrc = "one.txt", }, |
| }; |
| |
| static const QFileMonitorTestOp ops_touch_one[] = { |
| { .type = QFILE_MONITOR_TEST_OP_CREATE, |
| .filesrc = "one.txt", }, |
| { .type = QFILE_MONITOR_TEST_OP_TOUCH, |
| .filesrc = "one.txt", }, |
| }; |
| |
| |
| /* |
| * No we define data sets for the combinatorial |
| * expansion of file watches and operation sets |
| */ |
| #define PLAN_DATA(o, w) \ |
| static const QFileMonitorTestPlan plan_ ## o ## _ ## w = { \ |
| .nops = G_N_ELEMENTS(ops_ ##o), \ |
| .ops = ops_ ##o, \ |
| .nwatches = G_N_ELEMENTS(watches_ ##w), \ |
| .watches = watches_ ## w, \ |
| } |
| |
| PLAN_DATA(create_one, any); |
| PLAN_DATA(create_one, one); |
| PLAN_DATA(create_one, two); |
| PLAN_DATA(create_one, many); |
| |
| PLAN_DATA(delete_one, any); |
| PLAN_DATA(delete_one, one); |
| PLAN_DATA(delete_one, two); |
| PLAN_DATA(delete_one, many); |
| |
| PLAN_DATA(create_many, any); |
| PLAN_DATA(create_many, one); |
| PLAN_DATA(create_many, two); |
| PLAN_DATA(create_many, many); |
| |
| PLAN_DATA(rename_one, any); |
| PLAN_DATA(rename_one, one); |
| PLAN_DATA(rename_one, two); |
| PLAN_DATA(rename_one, many); |
| |
| PLAN_DATA(rename_many, any); |
| PLAN_DATA(rename_many, one); |
| PLAN_DATA(rename_many, two); |
| PLAN_DATA(rename_many, many); |
| |
| PLAN_DATA(append_one, any); |
| PLAN_DATA(append_one, one); |
| PLAN_DATA(append_one, two); |
| PLAN_DATA(append_one, many); |
| |
| PLAN_DATA(trunc_one, any); |
| PLAN_DATA(trunc_one, one); |
| PLAN_DATA(trunc_one, two); |
| PLAN_DATA(trunc_one, many); |
| |
| PLAN_DATA(touch_one, any); |
| PLAN_DATA(touch_one, one); |
| PLAN_DATA(touch_one, two); |
| PLAN_DATA(touch_one, many); |
| |
| |
| int main(int argc, char **argv) |
| { |
| g_test_init(&argc, &argv, NULL); |
| |
| qemu_init_main_loop(&error_abort); |
| |
| qemu_mutex_init(&evlock); |
| |
| /* |
| * Register test cases for the combinatorial |
| * expansion of file watches and operation sets |
| */ |
| #define PLAN_REGISTER(o, w) \ |
| g_test_add_data_func("/util/filemonitor/" # o "/" # w, \ |
| &plan_ ## o ## _ ## w, test_file_monitor_events) |
| |
| PLAN_REGISTER(create_one, any); |
| PLAN_REGISTER(create_one, one); |
| PLAN_REGISTER(create_one, two); |
| PLAN_REGISTER(create_one, many); |
| |
| PLAN_REGISTER(delete_one, any); |
| PLAN_REGISTER(delete_one, one); |
| PLAN_REGISTER(delete_one, two); |
| PLAN_REGISTER(delete_one, many); |
| |
| PLAN_REGISTER(create_many, any); |
| PLAN_REGISTER(create_many, one); |
| PLAN_REGISTER(create_many, two); |
| PLAN_REGISTER(create_many, many); |
| |
| PLAN_REGISTER(rename_one, any); |
| PLAN_REGISTER(rename_one, one); |
| PLAN_REGISTER(rename_one, two); |
| PLAN_REGISTER(rename_one, many); |
| |
| PLAN_REGISTER(rename_many, any); |
| PLAN_REGISTER(rename_many, one); |
| PLAN_REGISTER(rename_many, two); |
| PLAN_REGISTER(rename_many, many); |
| |
| PLAN_REGISTER(append_one, any); |
| PLAN_REGISTER(append_one, one); |
| PLAN_REGISTER(append_one, two); |
| PLAN_REGISTER(append_one, many); |
| |
| PLAN_REGISTER(trunc_one, any); |
| PLAN_REGISTER(trunc_one, one); |
| PLAN_REGISTER(trunc_one, two); |
| PLAN_REGISTER(trunc_one, many); |
| |
| PLAN_REGISTER(touch_one, any); |
| PLAN_REGISTER(touch_one, one); |
| PLAN_REGISTER(touch_one, two); |
| PLAN_REGISTER(touch_one, many); |
| |
| return g_test_run(); |
| } |