/* * Copyright (C) 2022 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 program; if not, see . * */ #include "config.h" #include #include #include #include #include #include #include "backends/meta-fd-source.h" #include "meta-dbus-input-capture.h" typedef struct { unsigned int width; unsigned int height; int x; int y; } Zone; G_DEFINE_AUTOPTR_CLEANUP_FUNC (Zone, g_free) typedef enum _Capabilities { CAPABILITY_NONE = 0, CAPABILITY_KEYBOARD = 1, CAPABILITY_POINTER = 2, CAPABILITY_TOUCH = 4, } Capabilities; typedef struct _InputCapture { MetaDBusInputCapture *proxy; } InputCapture; typedef struct _Event { enum ei_event_type type; struct { double dx; double dy; } motion; struct { uint32_t button; gboolean is_press; } button; struct { uint32_t key; gboolean is_press; } key; } Event; typedef struct _InputCaptureSession { MetaDBusInputCaptureSession *proxy; unsigned int serial; struct ei *ei; GSource *ei_source; Event *expected_events; int n_expected_events; int next_event; gboolean has_pointer; gboolean has_keyboard; } InputCaptureSession; static GDataInputStream *stdin_reader; static void ping_mutter (InputCaptureSession *session) { GDBusProxy *proxy = G_DBUS_PROXY (session->proxy); GError *error = NULL; if (!g_dbus_connection_call_sync (g_dbus_proxy_get_connection (proxy), "org.gnome.Mutter.InputCapture", g_dbus_proxy_get_object_path (proxy), "org.freedesktop.DBus.Peer", "Ping", NULL, NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, &error)) g_error ("Failed to ping D-Bus peer: %s", error->message); } static void write_state (InputCaptureSession *session, const char *state) { ping_mutter (session); fprintf (stdout, "%s\n", state); fflush (stdout); } typedef struct { GMainLoop *loop; const char *expected_state; } WaitData; static void on_line_read (GObject *source_object, GAsyncResult *res, gpointer user_data) { WaitData *data = user_data; g_autofree char *line = NULL; g_autoptr (GError) error = NULL; line = g_data_input_stream_read_line_finish (G_DATA_INPUT_STREAM (source_object), res, NULL, &error); if (error) g_error ("Failed to read line from test client: %s", error->message); if (!line) g_error ("Unexpected EOF"); g_assert_cmpstr (data->expected_state, ==, line); g_main_loop_quit (data->loop); } static void wait_for_state (InputCaptureSession *session, const char *expected_state) { WaitData data; data.loop = g_main_loop_new (NULL, FALSE); data.expected_state = expected_state; g_data_input_stream_read_line_async (stdin_reader, G_PRIORITY_DEFAULT, NULL, on_line_read, &data); g_main_loop_run (data.loop); g_main_loop_unref (data.loop); ping_mutter (session); } static InputCapture * input_capture_new (void) { InputCapture *input_capture; GError *error = NULL; input_capture = g_new0 (InputCapture, 1); input_capture->proxy = meta_dbus_input_capture_proxy_new_for_bus_sync ( G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, "org.gnome.Mutter.InputCapture", "/org/gnome/Mutter/InputCapture", NULL, &error); if (!input_capture->proxy) g_error ("Failed to acquire proxy: %s", error->message); return input_capture; } static InputCaptureSession * input_capture_create_session (InputCapture *input_capture) { GError *error = NULL; InputCaptureSession *session; g_autofree char *session_path = NULL; if (!meta_dbus_input_capture_call_create_session_sync (input_capture->proxy, CAPABILITY_POINTER, &session_path, NULL, &error)) g_error ("Failed to create input capture session: %s", error->message); session = g_new0 (InputCaptureSession, 1); session->proxy = meta_dbus_input_capture_session_proxy_new_for_bus_sync ( G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, "org.gnome.Mutter.InputCapture", session_path, NULL, &error); if (!session->proxy) g_error ("Failed to acquire proxy: %s", error->message); return session; } static void input_capture_session_close (InputCaptureSession *session) { GError *error = NULL; g_clear_pointer (&session->ei, ei_unref); g_clear_pointer (&session->ei_source, g_source_destroy); if (!meta_dbus_input_capture_session_call_close_sync (session->proxy, NULL, &error)) g_error ("Failed to close session: %s", error->message); g_object_unref (session->proxy); g_free (session); } static void record_event (InputCaptureSession *session, const Event *event) { const Event *expected_event; g_debug ("Record event #%d, with type %s", session->next_event + 1, ei_event_type_to_string (event->type)); g_assert_nonnull (session->expected_events); g_assert_cmpint (session->next_event, <, session->n_expected_events); expected_event = &session->expected_events[session->next_event++]; g_assert_cmpint (expected_event->type, ==, event->type); switch (event->type) { case EI_EVENT_POINTER_MOTION: g_assert_cmpfloat_with_epsilon (event->motion.dx, expected_event->motion.dx, DBL_EPSILON); g_assert_cmpfloat_with_epsilon (event->motion.dy, expected_event->motion.dy, DBL_EPSILON); break; case EI_EVENT_BUTTON_BUTTON: g_assert_cmpint (event->button.button, ==, expected_event->button.button); break; case EI_EVENT_KEYBOARD_KEY: g_assert_cmpint (event->key.key, ==, expected_event->key.key); break; case EI_EVENT_FRAME: break; default: break; } } static void process_ei_event (InputCaptureSession *session, struct ei_event *ei_event) { g_debug ("Processing event %s", ei_event_type_to_string (ei_event_get_type (ei_event))); switch (ei_event_get_type (ei_event)) { case EI_EVENT_SEAT_ADDED: { struct ei_seat *ei_seat = ei_event_get_seat (ei_event); g_assert_true (ei_seat_has_capability (ei_seat, EI_DEVICE_CAP_POINTER)); g_assert_true (ei_seat_has_capability (ei_seat, EI_DEVICE_CAP_KEYBOARD)); g_assert_true (ei_seat_has_capability (ei_seat, EI_DEVICE_CAP_BUTTON)); g_assert_true (ei_seat_has_capability (ei_seat, EI_DEVICE_CAP_SCROLL)); ei_seat_bind_capabilities (ei_seat, EI_DEVICE_CAP_POINTER, EI_DEVICE_CAP_BUTTON, EI_DEVICE_CAP_SCROLL, EI_DEVICE_CAP_KEYBOARD, NULL); break; } case EI_EVENT_DEVICE_ADDED: { struct ei_device *ei_device = ei_event_get_device (ei_event); if (ei_device_has_capability (ei_device, EI_DEVICE_CAP_POINTER) && ei_device_has_capability (ei_device, EI_DEVICE_CAP_BUTTON) && ei_device_has_capability (ei_device, EI_DEVICE_CAP_SCROLL)) session->has_pointer = TRUE; if (ei_device_has_capability (ei_device, EI_DEVICE_CAP_KEYBOARD)) session->has_keyboard = TRUE; break; } case EI_EVENT_DEVICE_REMOVED: { struct ei_device *ei_device = ei_event_get_device (ei_event); if (ei_device_has_capability (ei_device, EI_DEVICE_CAP_POINTER) && ei_device_has_capability (ei_device, EI_DEVICE_CAP_BUTTON) && ei_device_has_capability (ei_device, EI_DEVICE_CAP_SCROLL)) session->has_pointer = FALSE; if (ei_device_has_capability (ei_device, EI_DEVICE_CAP_KEYBOARD)) session->has_keyboard = FALSE; break; } case EI_EVENT_POINTER_MOTION: record_event (session, &(Event) { .type = EI_EVENT_POINTER_MOTION, .motion.dx = ei_event_pointer_get_dx (ei_event), .motion.dy = ei_event_pointer_get_dy (ei_event), }); break; case EI_EVENT_BUTTON_BUTTON: record_event (session, &(Event) { .type = EI_EVENT_BUTTON_BUTTON, .button.button = ei_event_button_get_button (ei_event), }); break; case EI_EVENT_KEYBOARD_KEY: record_event (session, &(Event) { .type = EI_EVENT_KEYBOARD_KEY, .key.key = ei_event_keyboard_get_key (ei_event), }); break; case EI_EVENT_FRAME: record_event (session, &(Event) { .type = EI_EVENT_FRAME }); break; default: break; } } static gboolean ei_source_prepare (gpointer user_data) { InputCaptureSession *session = user_data; struct ei_event *ei_event; gboolean retval; ei_event = ei_peek_event (session->ei); retval = !!ei_event; ei_event_unref (ei_event); return retval; } static gboolean ei_source_dispatch (gpointer user_data) { InputCaptureSession *session = user_data; ei_dispatch (session->ei); while (TRUE) { struct ei_event *ei_event; ei_event = ei_get_event (session->ei); if (!ei_event) break; process_ei_event (session, ei_event); ei_event_unref (ei_event); } return G_SOURCE_CONTINUE; } static void set_expected_events (InputCaptureSession *session, Event *expected_events, int n_expected_events) { session->expected_events = expected_events; session->n_expected_events = n_expected_events; session->next_event = 0; } static void log_handler (struct ei *ei, enum ei_log_priority priority, const char *message, struct ei_log_context *ctx) { int message_length = strlen (message); if (priority >= EI_LOG_PRIORITY_ERROR) g_critical ("libei: %.*s", message_length, message); else if (priority >= EI_LOG_PRIORITY_WARNING) g_warning ("libei: %.*s", message_length, message); else if (priority >= EI_LOG_PRIORITY_INFO) g_info ("libei: %.*s", message_length, message); else g_debug ("libei: %.*s", message_length, message); } static void input_capture_session_connect_to_eis (InputCaptureSession *session) { g_autoptr (GVariant) fd_variant = NULL; g_autoptr (GUnixFDList) fd_list = NULL; GError *error = NULL; int fd; struct ei *ei; int ret; if (!meta_dbus_input_capture_session_call_connect_to_eis_sync (session->proxy, NULL, &fd_variant, &fd_list, NULL, &error)) g_error ("Failed to connect to EIS: %s", error->message); fd = g_unix_fd_list_get (fd_list, g_variant_get_handle (fd_variant), &error); if (fd == -1) g_error ("Failed to get EIS file descriptor: %s", error->message); ei = ei_new_receiver (session); ei_log_set_handler (ei, log_handler); ei_log_set_priority (ei, EI_LOG_PRIORITY_DEBUG); ret = ei_setup_backend_fd (ei, fd); if (ret < 0) g_error ("Failed to setup libei backend: %s", g_strerror (errno)); session->ei = ei; session->ei_source = meta_create_fd_source (ei_get_fd (ei), "libei", ei_source_prepare, ei_source_dispatch, session, NULL); g_source_attach (session->ei_source, NULL); g_source_unref (session->ei_source); } static GList * input_capture_session_get_zones (InputCaptureSession *session) { GError *error = NULL; g_autoptr (GVariant) zones_variant = NULL; GVariantIter iter; GList *zones = NULL; unsigned int width, height; int x, y; if (!meta_dbus_input_capture_session_call_get_zones_sync (session->proxy, &session->serial, &zones_variant, NULL, &error)) g_error ("Failed to get zones: %s", error->message); g_variant_iter_init (&iter, zones_variant); while (g_variant_iter_next (&iter, "(uuii)", &width, &height, &x, &y)) { Zone *zone; zone = g_new0 (Zone, 1); *zone = (Zone) { .width = width, .height = height, .x = x, .y = y, }; zones = g_list_append (zones, zone); } return zones; } static unsigned int input_capture_session_add_barrier (InputCaptureSession *session, int x1, int y1, int x2, int y2) { g_autoptr (GError) error = NULL; unsigned int barrier_id; if (!meta_dbus_input_capture_session_call_add_barrier_sync ( session->proxy, session->serial, g_variant_new ("(iiii)", x1, y1, x2, y2), &barrier_id, NULL, &error)) { g_warning ("Failed to add barrier: %s", error->message); return 0; } return barrier_id; } static void input_capture_session_clear_barriers (InputCaptureSession *session) { g_autoptr (GError) error = NULL; if (!meta_dbus_input_capture_session_call_clear_barriers_sync ( session->proxy, NULL, &error)) g_warning ("Failed to clear barriers: %s", error->message); } static void input_capture_session_enable (InputCaptureSession *session) { g_autoptr (GError) error = NULL; if (!meta_dbus_input_capture_session_call_enable_sync (session->proxy, NULL, &error)) g_warning ("Failed to enable session: %s", error->message); } static void input_capture_session_disable (InputCaptureSession *session) { g_autoptr (GError) error = NULL; if (!meta_dbus_input_capture_session_call_disable_sync (session->proxy, NULL, &error)) g_warning ("Failed to disable session: %s", error->message); } static void input_capture_session_release (InputCaptureSession *session, double x, double y) { g_autoptr (GError) error = NULL; GVariantBuilder options_builder; g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT); g_variant_builder_add (&options_builder, "{sv}", "cursor_position", g_variant_new ("(dd)", x, y)); if (!meta_dbus_input_capture_session_call_release_sync (session->proxy, g_variant_builder_end (&options_builder), NULL, &error)) g_warning ("Failed to release pointer: %s", error->message); } static void test_sanity (void) { InputCapture *input_capture; InputCaptureSession *session; input_capture = input_capture_new (); session = input_capture_create_session (input_capture); g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "*org.freedesktop.DBus.Error.Failed: Session not enabled*"); input_capture_session_disable (session); g_test_assert_expected_messages (); input_capture_session_enable (session); g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "*org.freedesktop.DBus.Error.Failed: Already enabled*"); input_capture_session_enable (session); g_test_assert_expected_messages (); input_capture_session_disable (session); g_test_expect_message (G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "*org.freedesktop.DBus.Error.Failed: Session not enabled*"); input_capture_session_disable (session); g_test_assert_expected_messages (); input_capture_session_close (session); } static void on_zones_changed (MetaDBusInputCaptureSession *proxy, int *zones_changed_count) { *zones_changed_count += 1; } static void assert_zones (GList *zones, const Zone *expected_zones, int n_expected_zones) { GList *l; int i; g_assert_cmpuint (g_list_length (zones), ==, n_expected_zones); for (l = zones, i = 0; l; l = l->next, i++) { Zone *zone = l->data; g_assert_cmpint (zone->width, ==, expected_zones[i].width); g_assert_cmpint (zone->height, ==, expected_zones[i].height); g_assert_cmpint (zone->x, ==, expected_zones[i].x); g_assert_cmpint (zone->y, ==, expected_zones[i].y); } } static void test_zones (void) { InputCapture *input_capture; InputCaptureSession *session; static const Zone expected_zones1[] = { { .width = 800, .height = 600, .x = 0, .y = 0 }, { .width = 1024, .height = 768, .x = 800, .y = 0 }, }; static const Zone expected_zones2[] = { { .width = 1024, .height = 768, .x = 0, .y = 0 }, }; GList *zones; int zones_changed_count = 0; unsigned int serial; input_capture = input_capture_new (); session = input_capture_create_session (input_capture); g_signal_connect (session->proxy, "zones-changed", G_CALLBACK (on_zones_changed), &zones_changed_count); zones = input_capture_session_get_zones (session); assert_zones (zones, expected_zones1, G_N_ELEMENTS (expected_zones1)); g_clear_list (&zones, g_free); write_state (session, "1"); while (zones_changed_count == 0) g_main_context_iteration (NULL, TRUE); serial = session->serial; g_clear_list (&zones, g_free); zones = input_capture_session_get_zones (session); g_assert_cmpuint (session->serial, >, serial); assert_zones (zones, expected_zones2, G_N_ELEMENTS (expected_zones2)); g_clear_list (&zones, g_free); input_capture_session_close (session); } typedef struct { unsigned int activated_barrier_id; double activated_x; double activated_y; unsigned int activated_serial; } BarriersTestData; static void on_activated (MetaDBusInputCaptureSession *proxy, unsigned int barrier_id, unsigned int serial, GVariant *cursor_position, BarriersTestData *data) { g_assert_cmpuint (data->activated_barrier_id, ==, 0); data->activated_barrier_id = barrier_id; data->activated_serial = serial; g_variant_get (cursor_position, "(dd)", &data->activated_x, &data->activated_y); } static void test_barriers (void) { InputCapture *input_capture; InputCaptureSession *session; g_autolist (Zone) zones = NULL; unsigned int barrier1, barrier2, barrier3; BarriersTestData data = {}; unsigned int prev_activated_serial; input_capture = input_capture_new (); session = input_capture_create_session (input_capture); zones = input_capture_session_get_zones (session); /* * +-------------+==============+ * || | ^ | * ||<--B#1 | | | * || | B#2 B#3 | * +-------------+ | | * | V | * +==============+ */ barrier1 = input_capture_session_add_barrier (session, 0, 0, 0, 600); barrier2 = input_capture_session_add_barrier (session, 800, 768, 1823, 768); barrier3 = input_capture_session_add_barrier (session, 800, 0, 1823, 0); g_assert_cmpuint (barrier1, !=, 0); g_assert_cmpuint (barrier2, !=, 0); g_assert_cmpuint (barrier3, !=, 0); g_assert_cmpuint (barrier1, !=, barrier2); g_assert_cmpuint (barrier1, !=, barrier3); g_assert_cmpuint (barrier2, !=, barrier3); g_signal_connect (session->proxy, "activated", G_CALLBACK (on_activated), &data); input_capture_session_enable (session); write_state (session, "1"); while (data.activated_barrier_id == 0) g_main_context_iteration (NULL, TRUE); g_assert_cmpuint (data.activated_serial, !=, 0); g_assert_cmpuint (data.activated_barrier_id, ==, barrier1); g_assert_cmpfloat_with_epsilon (data.activated_x, 0.0, DBL_EPSILON); g_assert_cmpfloat_with_epsilon (data.activated_y, 15.0, DBL_EPSILON); wait_for_state (session, "1"); input_capture_session_release (session, 200, 150); write_state (session, "2"); prev_activated_serial = data.activated_serial; data = (BarriersTestData) {}; while (data.activated_barrier_id == 0) g_main_context_iteration (NULL, TRUE); g_assert_cmpuint (data.activated_serial, !=, 0); g_assert_cmpuint (data.activated_serial, !=, prev_activated_serial); g_assert_cmpuint (data.activated_barrier_id, ==, barrier2); g_assert_cmpfloat_with_epsilon (data.activated_x, 1000.0, DBL_EPSILON); g_assert_cmpfloat_with_epsilon (data.activated_y, 768.0, DBL_EPSILON); input_capture_session_release (session, 1200, 700); write_state (session, "3"); input_capture_session_close (session); } static void test_clear_barriers (void) { InputCapture *input_capture; InputCaptureSession *session; g_autolist (Zone) zones = NULL; BarriersTestData data = {}; input_capture = input_capture_new (); session = input_capture_create_session (input_capture); zones = input_capture_session_get_zones (session); input_capture_session_add_barrier (session, 0, 0, 0, 600); g_signal_connect (session->proxy, "activated", G_CALLBACK (on_activated), &data); input_capture_session_enable (session); write_state (session, "1"); while (data.activated_barrier_id == 0) g_main_context_iteration (NULL, TRUE); input_capture_session_clear_barriers (session); write_state (session, "2"); wait_for_state (session, "1"); input_capture_session_close (session); } static void test_cancel_keybinding (void) { InputCapture *input_capture; InputCaptureSession *session; g_autolist (Zone) zones = NULL; input_capture = input_capture_new (); session = input_capture_create_session (input_capture); zones = input_capture_session_get_zones (session); input_capture_session_add_barrier (session, 0, 0, 0, 600); input_capture_session_enable (session); write_state (session, "1"); wait_for_state (session, "1"); input_capture_session_close (session); } static void test_events (void) { InputCapture *input_capture; InputCaptureSession *session; g_autolist (Zone) zones = NULL; Event expected_events[] = { /* Move the pointer with deltas (10, 15) and (2, -5), then click */ { .type = EI_EVENT_POINTER_MOTION, .motion = { .dx = -10.0, .dy = -10.0 }, }, { .type = EI_EVENT_FRAME, }, { .type = EI_EVENT_POINTER_MOTION, .motion = { .dx = 2.0, .dy = -5.0 }, }, { .type = EI_EVENT_FRAME, }, { .type = EI_EVENT_BUTTON_BUTTON, .button = { .button = BTN_LEFT, .is_press = TRUE }, }, { .type = EI_EVENT_FRAME, }, { .type = EI_EVENT_BUTTON_BUTTON, .button = { .button = BTN_LEFT, .is_press = FALSE }, }, { .type = EI_EVENT_FRAME, }, /* Press, then release, KEY_A */ { .type = EI_EVENT_KEYBOARD_KEY, .key = { .key = KEY_A, .is_press = TRUE }, }, { .type = EI_EVENT_FRAME, }, { .type = EI_EVENT_KEYBOARD_KEY, .key = { .key = KEY_A, .is_press = FALSE }, }, { .type = EI_EVENT_FRAME, }, }; input_capture = input_capture_new (); session = input_capture_create_session (input_capture); input_capture_session_connect_to_eis (session); zones = input_capture_session_get_zones (session); input_capture_session_add_barrier (session, 0, 0, 0, 600); input_capture_session_enable (session); while (!session->has_pointer || !session->has_keyboard) g_main_context_iteration (NULL, TRUE); write_state (session, "1"); set_expected_events (session, expected_events, G_N_ELEMENTS (expected_events)); while (session->next_event < session->n_expected_events) g_main_context_iteration (NULL, TRUE); input_capture_session_close (session); } static void test_a11y (void) { InputCapture *input_capture; InputCaptureSession *session; g_autolist (Zone) zones = NULL; Event expected_events[] = { { .type = EI_EVENT_POINTER_MOTION, .motion = { .dx = -10.0, .dy = 0.0 }, }, { .type = EI_EVENT_FRAME, }, { .type = EI_EVENT_BUTTON_BUTTON, .button = { .button = BTN_LEFT, .is_press = TRUE }, }, { .type = EI_EVENT_FRAME, }, { .type = EI_EVENT_BUTTON_BUTTON, .button = { .button = BTN_LEFT, .is_press = FALSE }, }, { .type = EI_EVENT_FRAME, }, { .type = EI_EVENT_KEYBOARD_KEY, .key = { .key = KEY_A, .is_press = TRUE }, }, { .type = EI_EVENT_FRAME, }, { .type = EI_EVENT_KEYBOARD_KEY, .key = { .key = KEY_A, .is_press = FALSE }, }, { .type = EI_EVENT_FRAME, }, }; input_capture = input_capture_new (); session = input_capture_create_session (input_capture); input_capture_session_connect_to_eis (session); zones = input_capture_session_get_zones (session); input_capture_session_add_barrier (session, 0, 0, 0, 600); input_capture_session_enable (session); set_expected_events (session, expected_events, G_N_ELEMENTS (expected_events)); write_state (session, "1"); while (session->next_event < session->n_expected_events) g_main_context_iteration (NULL, TRUE); wait_for_state (session, "1"); input_capture_session_close (session); } static const struct { const char *name; void (* func) (void); } test_cases[] = { { "sanity", test_sanity, }, { "zones", test_zones, }, { "barriers", test_barriers, }, { "clear-barriers", test_clear_barriers, }, { "cancel-keybinding", test_cancel_keybinding, }, { "events", test_events, }, { "a11y", test_a11y, }, }; static void print_to_stderr (const char *text) { fputs (text, stderr); fflush (stderr); } int main (int argc, char **argv) { const char *test_case; int i; g_assert_cmpint (argc, ==, 2); test_case = argv[1]; g_set_print_handler (print_to_stderr); g_test_init (&argc, &argv, NULL); for (i = 0; i < G_N_ELEMENTS (test_cases); i++) { if (g_strcmp0 (test_cases[i].name, test_case) == 0) { g_autoptr (GInputStream) stdin_stream = NULL; stdin_stream = g_unix_input_stream_new (fileno (stdin), FALSE); stdin_reader = g_data_input_stream_new (stdin_stream); test_cases[i].func (); g_clear_object (&stdin_reader); g_clear_object (&stdin_stream); return EXIT_SUCCESS; } } g_warning ("Invalid test case '%s'", test_case); return EXIT_FAILURE; }