/* * Copyright (C) 2017 Christian J. Kellner * * SPDX-License-Identifier: LGPL-2.1+ */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "fu-context-private.h" #include "fu-plugin-private.h" #include "fu-thunderbolt-firmware.h" #include "fu-thunderbolt-firmware-update.h" #include "fu-udev-device-private.h" static gchar * udev_mock_add_domain (UMockdevTestbed *bed, int id) { gchar *path; g_autofree gchar *name = NULL; name = g_strdup_printf ("domain%d", id); path = umockdev_testbed_add_device (bed, "thunderbolt", name, NULL, "security", "secure", NULL, "DEVTYPE", "thunderbolt_domain", NULL); g_assert_nonnull (path); return path; } static gchar * udev_mock_add_nvmem (UMockdevTestbed *bed, gboolean active, const char *parent, int id) { g_autofree gchar *name = NULL; gchar *path; name = g_strdup_printf ("%s%d", active ? "nvm_active" : "nvm_non_active", id); path = umockdev_testbed_add_device (bed, "nvmem", name, parent, "nvmem", "", NULL, NULL); g_assert_nonnull (path); return path; } typedef struct MockDevice MockDevice; struct MockDevice { const char *name; /* sysfs: device_name */ const char *id; /* sysfs: device */ const char *nvm_version; const char *nvm_parsed_version; int delay_ms; int domain_id; struct MockDevice *children; /* optionally filled out */ const char *uuid; }; typedef struct MockTree MockTree; struct MockTree { const MockDevice *device; MockTree *parent; GPtrArray *children; gchar *sysfs_parent; int sysfs_id; int sysfs_nvm_id; gchar *uuid; UMockdevTestbed *bed; gchar *path; gchar *nvm_non_active; gchar *nvm_active; guint nvm_authenticate; gchar *nvm_version; FuDevice *fu_device; }; static MockTree * mock_tree_new (MockTree *parent, MockDevice *device, int *id) { MockTree *node = g_slice_new0 (MockTree); int current_id = (*id)++; node->device = device; node->sysfs_id = current_id; node->sysfs_nvm_id = current_id; node->parent = parent; if (device->uuid) node->uuid = g_strdup (device->uuid); else node->uuid = g_uuid_string_random (); node->nvm_version = g_strdup (device->nvm_version); return node; } static void mock_tree_free (MockTree *tree) { for (guint i = 0; i < tree->children->len; i++) { MockTree *child = g_ptr_array_index (tree->children, i); mock_tree_free (child); } g_ptr_array_free (tree->children, TRUE); if (tree->fu_device) g_object_unref (tree->fu_device); g_free (tree->uuid); if (tree->bed != NULL) { if (tree->nvm_active) { umockdev_testbed_uevent (tree->bed, tree->nvm_active, "remove"); umockdev_testbed_remove_device (tree->bed, tree->nvm_active); } if (tree->nvm_non_active) { umockdev_testbed_uevent (tree->bed, tree->nvm_non_active, "remove"); umockdev_testbed_remove_device (tree->bed, tree->nvm_non_active); } if (tree->path) { umockdev_testbed_uevent (tree->bed, tree->path, "remove"); umockdev_testbed_remove_device (tree->bed, tree->path); } g_object_unref (tree->bed); } g_free (tree->nvm_version); g_free (tree->nvm_active); g_free (tree->nvm_non_active); g_free (tree->path); g_free (tree->sysfs_parent); g_slice_free (MockTree, tree); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC (MockTree, mock_tree_free); #pragma clang diagnostic pop static GPtrArray * mock_tree_init_children (MockTree *node, int *id) { GPtrArray *children = g_ptr_array_new (); MockDevice *iter; for (iter = node->device->children; iter && iter->name; iter++) { MockTree *child = mock_tree_new (node, iter, id); g_ptr_array_add (children, child); child->children = mock_tree_init_children (child, id); } return children; } static MockTree * mock_tree_init (MockDevice *device) { MockTree *tree; int devices = 0; tree = mock_tree_new (NULL, device, &devices); tree->children = mock_tree_init_children (tree, &devices); return tree; } static void mock_tree_dump (const MockTree *node, int level) { if (node->path) { g_debug ("%*s * %s [%s] at %s", level, " ", node->device->name, node->uuid, node->path); g_debug ("%*s non-active nvmem at %s", level, " ", node->nvm_non_active); g_debug ("%*s active nvmem at %s", level, " ", node->nvm_active); } else { g_debug ("%*s * %s [%s] %d", level, " ", node->device->name, node->uuid, node->sysfs_id); } for (guint i = 0; i < node->children->len; i++) { const MockTree *child = g_ptr_array_index (node->children, i); mock_tree_dump (child, level + 2); } } static void mock_tree_firmware_verify (const MockTree *node, GBytes *data) { g_autoptr(GFile) nvm_device = NULL; g_autoptr(GFile) nvm = NULL; g_autoptr(GInputStream) is = NULL; g_autoptr(GChecksum) chk = NULL; g_autoptr(GError) error = NULL; g_autofree gchar *sum_data = NULL; const gchar *sum_disk = NULL; gsize s; sum_data = g_compute_checksum_for_bytes (G_CHECKSUM_SHA1, data); chk = g_checksum_new (G_CHECKSUM_SHA1); g_assert_nonnull (node); g_assert_nonnull (node->nvm_non_active); nvm_device = g_file_new_for_path (node->nvm_non_active); nvm = g_file_get_child (nvm_device, "nvmem"); is = (GInputStream *) g_file_read (nvm, NULL, &error); g_assert_no_error (error); g_assert_nonnull (is); do { g_autoptr(GBytes) b = NULL; const guchar *d; b = g_input_stream_read_bytes (is, 4096, NULL, &error); g_assert_no_error (error); g_assert_nonnull (is); d = g_bytes_get_data (b, &s); if (s > 0) g_checksum_update (chk, d, (gssize) s); } while (s > 0); sum_disk = g_checksum_get_string (chk); g_assert_cmpstr (sum_data, ==, sum_disk); } typedef gboolean (* MockTreePredicate) (const MockTree *node, gpointer data); static const MockTree * mock_tree_contains (const MockTree *node, MockTreePredicate predicate, gpointer data) { if (predicate (node, data)) return node; for (guint i = 0; i < node->children->len; i++) { const MockTree *child; const MockTree *match; child = g_ptr_array_index (node->children, i); match = mock_tree_contains (child, predicate, data); if (match != NULL) return match; } return NULL; } static gboolean mock_tree_all (const MockTree *node, MockTreePredicate predicate, gpointer data) { if (!predicate (node, data)) return FALSE; for (guint i = 0; i < node->children->len; i++) { const MockTree *child; child = g_ptr_array_index (node->children, i); if (!mock_tree_all (child, predicate, data)) return FALSE; } return TRUE; } static gboolean mock_tree_compare_uuid (const MockTree *node, gpointer data) { const gchar *uuid = (const gchar *) data; return g_str_equal (node->uuid, uuid); } static const MockTree * mock_tree_find_uuid (const MockTree *root, const char *uuid) { return mock_tree_contains (root, mock_tree_compare_uuid, (gpointer) uuid); } static gboolean mock_tree_node_have_fu_device (const MockTree *node, gpointer data) { return node->fu_device != NULL; } static void write_controller_fw (const gchar *nvm) { g_autoptr(GFile) nvm_device = NULL; g_autoptr(GFile) nvmem = NULL; g_autofree gchar *fw_path = NULL; g_autoptr(GFile) fw_file = NULL; g_autoptr(GInputStream) is = NULL; g_autoptr(GOutputStream) os = NULL; g_autoptr(GError) error = NULL; gssize n; fw_path = g_build_filename (TESTDATADIR, "thunderbolt/minimal-fw-controller.bin", NULL); fw_file = g_file_new_for_path (fw_path); g_assert_nonnull (fw_file); nvm_device = g_file_new_for_path (nvm); g_assert_nonnull (nvm_device); nvmem = g_file_get_child (nvm_device, "nvmem"); g_assert_nonnull (nvmem); os = (GOutputStream *) g_file_append_to (nvmem, G_FILE_CREATE_NONE, NULL, &error); g_assert_no_error (error); g_assert_nonnull (os); is = (GInputStream *) g_file_read (fw_file, NULL, &error); g_assert_no_error (error); g_assert_nonnull (is); n = g_output_stream_splice (os, is, G_OUTPUT_STREAM_SPLICE_NONE, NULL, &error); g_assert_no_error (error); g_assert_cmpuint (n, >, 0); } static gboolean mock_tree_attach_device (gpointer user_data) { MockTree *tree = (MockTree *) user_data; const MockDevice *dev = tree->device; g_autofree gchar *idstr = NULL; g_autofree gchar *authenticate = NULL; g_assert_nonnull (tree); g_assert_nonnull (tree->sysfs_parent); g_assert_nonnull (dev); idstr = g_strdup_printf ("%d-%d", dev->domain_id, tree->sysfs_id); authenticate = g_strdup_printf ("0x%x", tree->nvm_authenticate); tree->path = umockdev_testbed_add_device (tree->bed, "thunderbolt", idstr, tree->sysfs_parent, "device_name", dev->name, "device", dev->id, "vendor", "042", "vendor_name", "GNOME.org", "authorized", "1", "nvm_authenticate", authenticate, "nvm_version", tree->nvm_version, "unique_id", tree->uuid, NULL, "DEVTYPE", "thunderbolt_device", NULL); tree->nvm_non_active = udev_mock_add_nvmem (tree->bed, FALSE, tree->path, tree->sysfs_id); tree->nvm_active = udev_mock_add_nvmem (tree->bed, TRUE, tree->path, tree->sysfs_id); g_assert_nonnull (tree->path); g_assert_nonnull (tree->nvm_non_active); g_assert_nonnull (tree->nvm_active); write_controller_fw (tree->nvm_active); for (guint i = 0; i < tree->children->len; i++) { MockTree *child; child = g_ptr_array_index (tree->children, i); child->bed = g_object_ref (tree->bed); child->sysfs_parent = g_strdup (tree->path); g_timeout_add (child->device->delay_ms, mock_tree_attach_device, child); } return FALSE; } typedef struct SyncContext { MockTree *tree; GMainLoop *loop; } SyncContext; static gboolean on_sync_timeout (gpointer user_data) { SyncContext *ctx = (SyncContext *) user_data; g_main_loop_quit (ctx->loop); return FALSE; } static void sync_device_added (FuPlugin *plugin, FuDevice *device, gpointer user_data) { SyncContext *ctx = (SyncContext *) user_data; MockTree *tree = ctx->tree; const gchar *uuid = fu_device_get_physical_id (device); MockTree *target; target = (MockTree *) mock_tree_find_uuid (tree, uuid); if (target == NULL) { g_critical ("Got device that could not be matched: %s", uuid); return; } if (target->fu_device != NULL) g_object_unref (target->fu_device); target->fu_device = g_object_ref (device); } static void sync_device_removed (FuPlugin *plugin, FuDevice *device, gpointer user_data) { SyncContext *ctx = (SyncContext *) user_data; MockTree *tree = ctx->tree; const gchar *uuid = fu_device_get_physical_id (device); MockTree *target; target = (MockTree *) mock_tree_find_uuid (tree, uuid); if (target == NULL) { g_warning ("Got device that could not be matched: %s", uuid); return; } else if (target->fu_device == NULL) { g_warning ("Got remove event for out-of-tree device %s", uuid); return; } g_object_unref (target->fu_device); target->fu_device = NULL; } static void mock_tree_sync (MockTree *root, FuPlugin *plugin, int timeout_ms) { g_autoptr(GMainLoop) mainloop = g_main_loop_new (NULL, FALSE); gulong id_add; gulong id_del; SyncContext ctx = { .tree = root, .loop = mainloop, }; id_add = g_signal_connect (plugin, "device-added", G_CALLBACK (sync_device_added), &ctx); id_del = g_signal_connect (plugin, "device-removed", G_CALLBACK (sync_device_removed), &ctx); if (timeout_ms > 0) g_timeout_add (timeout_ms, on_sync_timeout, &ctx); g_main_loop_run (mainloop); g_signal_handler_disconnect (plugin, id_add); g_signal_handler_disconnect (plugin, id_del); } typedef struct AttachContext { /* in */ MockTree *tree; GMainLoop *loop; /* out */ gboolean complete; } AttachContext; static void mock_tree_plugin_device_added (FuPlugin *plugin, FuDevice *device, gpointer user_data) { AttachContext *ctx = (AttachContext *) user_data; MockTree *tree = ctx->tree; const gchar *uuid = fu_device_get_physical_id (device); MockTree *target; target = (MockTree *) mock_tree_find_uuid (tree, uuid); if (target == NULL) { g_warning ("Got device that could not be matched: %s", uuid); return; } g_set_object (&target->fu_device, device); if (mock_tree_all (tree, mock_tree_node_have_fu_device, NULL)) { ctx->complete = TRUE; g_main_loop_quit (ctx->loop); } } static gboolean mock_tree_settle (MockTree *root, FuPlugin *plugin) { g_autoptr(GMainLoop) mainloop = g_main_loop_new (NULL, FALSE); gulong id; AttachContext ctx = { .tree = root, .loop = mainloop, }; id = g_signal_connect (plugin, "device-added", G_CALLBACK (mock_tree_plugin_device_added), &ctx); g_main_loop_run (mainloop); g_signal_handler_disconnect (plugin, id); return ctx.complete; } static gboolean mock_tree_attach (MockTree *root, UMockdevTestbed *bed, FuPlugin *plugin) { root->bed = g_object_ref (bed); root->sysfs_parent = udev_mock_add_domain (bed, root->device->domain_id); g_assert_nonnull (root->sysfs_parent); g_timeout_add (root->device->delay_ms, mock_tree_attach_device, root); return mock_tree_settle (root, plugin); } /* the unused parameter makes the function signature compatible * with 'MockTreePredicate' */ static gboolean mock_tree_node_is_detached (const MockTree *node, gpointer unused) { gboolean ret = node->path == NULL; /* consistency checks: if ret, make sure we are * fully detached */ if (ret) { g_assert_null (node->nvm_active); g_assert_null (node->nvm_non_active); g_assert_null (node->bed); } else { g_assert_nonnull (node->nvm_active); g_assert_nonnull (node->nvm_non_active); g_assert_nonnull (node->bed); } return ret; } static void mock_tree_detach (MockTree *node) { UMockdevTestbed *bed; if (mock_tree_node_is_detached (node, NULL)) return; for (guint i = 0; i < node->children->len; i++) { MockTree *child = g_ptr_array_index (node->children, i); mock_tree_detach (child); g_free (child->sysfs_parent); child->sysfs_parent = NULL; } bed = node->bed; umockdev_testbed_uevent (bed, node->nvm_active, "remove"); umockdev_testbed_remove_device (bed, node->nvm_active); umockdev_testbed_uevent (bed, node->nvm_non_active, "remove"); umockdev_testbed_remove_device (bed, node->nvm_non_active); umockdev_testbed_uevent (bed, node->path, "remove"); umockdev_testbed_remove_device (bed, node->path); g_free (node->path); g_free (node->nvm_non_active); g_free (node->nvm_active); node->path = NULL; node->nvm_non_active = NULL; node->nvm_active = NULL; g_object_unref (bed); node->bed = NULL; } typedef enum UpdateResult { UPDATE_SUCCESS = 0, /* nvm_authenticate will report error condition */ UPDATE_FAIL_DEVICE_INTERNAL = 1, /* device to be updated will NOT re-appear */ UPDATE_FAIL_DEVICE_NOSHOW = 2 } UpdateResult; typedef struct UpdateContext { GFileMonitor *monitor; UpdateResult result; guint timeout; GBytes *data; UMockdevTestbed *bed; FuPlugin *plugin; MockTree *node; gchar *version; } UpdateContext; static void update_context_free (UpdateContext *ctx) { if (ctx == NULL) return; g_file_monitor_cancel (ctx->monitor); g_object_unref (ctx->bed); g_object_unref (ctx->plugin); g_object_unref (ctx->monitor); g_bytes_unref (ctx->data); g_free (ctx->version); g_free (ctx); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC (UpdateContext, update_context_free); #pragma clang diagnostic pop static gboolean reattach_tree (gpointer user_data) { UpdateContext *ctx = (UpdateContext *) user_data; MockTree *node = ctx->node; g_debug ("Mock update done, reattaching tree..."); node->bed = g_object_ref (ctx->bed); g_timeout_add (node->device->delay_ms, mock_tree_attach_device, node); return FALSE; } static void udev_file_changed_cb (GFileMonitor *monitor, GFile *file, GFile *other_file, GFileMonitorEvent event_type, gpointer user_data) { UpdateContext *ctx = (UpdateContext *) user_data; gboolean ok; gsize len; g_autofree gchar *data = NULL; g_autoptr(GError) error = NULL; g_debug ("Got update trigger"); ok = g_file_monitor_cancel (monitor); g_assert_true (ok); ok = g_file_load_contents (file, NULL, &data, &len, NULL, &error); g_assert_no_error (error); g_assert_true (ok); if (!g_str_has_prefix (data, "1")) return; /* verify the firmware is correct */ mock_tree_firmware_verify (ctx->node, ctx->data); g_debug ("Removing tree below and including: %s", ctx->node->path); mock_tree_detach (ctx->node); ctx->node->nvm_authenticate = (guint) ctx->result; /* update the version only on "success" simulations */ if (ctx->result == UPDATE_SUCCESS) { g_free (ctx->node->nvm_version); ctx->node->nvm_version = g_strdup (ctx->version); } g_debug ("Simulating update to '%s' with result: 0x%x", ctx->version, ctx->node->nvm_authenticate); if (ctx->result == UPDATE_FAIL_DEVICE_NOSHOW) { g_debug ("Simulating no-show fail:" " device tree will not reappear"); return; } g_debug ("Device tree reattachment in %3.2f seconds", ctx->timeout / 1000.0); g_timeout_add (ctx->timeout, reattach_tree, ctx); } static UpdateContext * mock_tree_prepare_for_update (MockTree *node, FuPlugin *plugin, const char *version, GBytes *fw_data, guint timeout_ms) { UpdateContext *ctx; g_autoptr(GFile) dir = NULL; g_autoptr(GFile) f = NULL; g_autoptr(GError) error = NULL; GFileMonitor *monitor; ctx = g_new0 (UpdateContext, 1); dir = g_file_new_for_path (node->path); f = g_file_get_child (dir, "nvm_authenticate"); monitor = g_file_monitor_file (f, G_FILE_MONITOR_NONE, NULL, &error); g_assert_no_error (error); g_assert_nonnull (monitor); ctx->node = node; ctx->plugin = g_object_ref (plugin); ctx->bed = g_object_ref (node->bed); ctx->timeout = timeout_ms; ctx->monitor = monitor; ctx->version = g_strdup (version); ctx->data = g_bytes_ref (fw_data); g_signal_connect (monitor, "changed", G_CALLBACK (udev_file_changed_cb), ctx); return ctx; } static MockDevice root_one = { .name = "Laptop", .id = "0x23", .nvm_version = "20.2", .nvm_parsed_version = "20.02", .children = (MockDevice[]) { { .name = "Thunderbolt Cable", .id = "0x24", .nvm_version = "20.0", .nvm_parsed_version = "20.00", .children = (MockDevice[]) { { .name = "Thunderbolt Dock", .id = "0x25", .nvm_version = "10.0", .nvm_parsed_version = "10.00", }, { NULL, } }, }, { .name = "Thunderbolt Cable", .id = "0x24", .nvm_version = "23.0", .nvm_parsed_version = "23.00", .children = (MockDevice[]) { { .name = "Thunderbolt SSD", .id = "0x26", .nvm_version = "5.0", .nvm_parsed_version = "05.00", }, { NULL, } }, }, { NULL, }, }, }; typedef struct TestParam { gboolean initialize_tree; gboolean attach_and_coldplug; const char *firmware_file; } TestParam; typedef enum TestFlags { TEST_INITIALIZE_TREE = 1 << 0, TEST_ATTACH = 1 << 1, TEST_PREPARE_FIRMWARE = 1 << 2, TEST_PREPARE_ALL = TEST_INITIALIZE_TREE | TEST_ATTACH | TEST_PREPARE_FIRMWARE } TestFlags; #define TEST_INIT_FULL (GUINT_TO_POINTER (TEST_PREPARE_ALL)) #define TEST_INIT_NONE (GUINT_TO_POINTER (0)) typedef struct ThunderboltTest { UMockdevTestbed *bed; FuPlugin *plugin; GUdevClient *udev_client; /* if TestParam::initialize_tree */ MockTree *tree; /* if TestParam::firmware_file is nonnull */ GMappedFile *fw_file; GBytes *fw_data; } ThunderboltTest; static void fu_thunderbolt_gudev_uevent_cb (GUdevClient *gudev_client, const gchar *action, GUdevDevice *udev_device, ThunderboltTest *tt) { if (g_strcmp0 (action, "add") == 0) { g_autoptr(FuUdevDevice) device = fu_udev_device_new (udev_device); g_autoptr(GError) error_local = NULL; if (!fu_plugin_runner_backend_device_added (tt->plugin, FU_DEVICE (device), &error_local)) g_debug ("failed to add: %s", error_local->message); return; } if (g_strcmp0 (action, "remove") == 0) { if (tt->tree->fu_device != NULL) fu_plugin_device_remove (tt->plugin, tt->tree->fu_device); return; } if (g_strcmp0 (action, "change") == 0) { const gchar *uuid = g_udev_device_get_sysfs_attr (udev_device, "unique_id"); MockTree *target = (MockTree *) mock_tree_find_uuid (tt->tree, uuid); g_assert_nonnull (target); fu_udev_device_emit_changed (FU_UDEV_DEVICE (target->fu_device)); return; } } static void test_set_up (ThunderboltTest *tt, gconstpointer params) { TestFlags flags = GPOINTER_TO_UINT(params); gboolean ret; g_autofree gchar *pluginfn = NULL; g_autofree gchar *sysfs = NULL; g_autoptr(GError) error = NULL; g_autoptr(FuContext) ctx = fu_context_new (); const gchar *udev_subsystems[] = { "thunderbolt", NULL }; ret = fu_context_load_quirks (ctx, FU_QUIRKS_LOAD_FLAG_NO_CACHE | FU_QUIRKS_LOAD_FLAG_NO_VERIFY, &error); g_assert_no_error (error); g_assert_true (ret); tt->bed = umockdev_testbed_new (); g_assert_nonnull (tt->bed); sysfs = umockdev_testbed_get_sys_dir (tt->bed); g_debug ("mock sysfs at %s", sysfs); tt->plugin = fu_plugin_new (ctx); g_assert_nonnull (tt->plugin); pluginfn = g_build_filename (PLUGINBUILDDIR, "libfu_plugin_thunderbolt." G_MODULE_SUFFIX, NULL); ret = fu_plugin_open (tt->plugin, pluginfn, &error); g_assert_no_error (error); g_assert_true (ret); ret = fu_plugin_runner_startup (tt->plugin, &error); g_assert_no_error (error); g_assert_true (ret); if (flags & TEST_INITIALIZE_TREE) { tt->tree = mock_tree_init (&root_one); g_assert_nonnull (tt->tree); } if (!umockdev_in_mock_environment ()) { g_warning ("Need to run with umockdev-wrapper"); return; } tt->udev_client = g_udev_client_new (udev_subsystems); g_assert_nonnull (tt->udev_client); g_signal_connect (tt->udev_client, "uevent", G_CALLBACK (fu_thunderbolt_gudev_uevent_cb), tt); if (flags & TEST_ATTACH) { g_assert_true (flags & TEST_INITIALIZE_TREE); ret = mock_tree_attach (tt->tree, tt->bed, tt->plugin); g_assert_true (ret); } if (flags & TEST_PREPARE_FIRMWARE) { g_autofree gchar *fw_path = NULL; fw_path = g_build_filename (TESTDATADIR, "thunderbolt/minimal-fw.bin", NULL); tt->fw_file = g_mapped_file_new (fw_path, FALSE, &error); g_assert_no_error (error); g_assert_nonnull (tt->fw_file); tt->fw_data = g_mapped_file_get_bytes (tt->fw_file); g_assert_nonnull (tt->fw_data); } } static void test_tear_down (ThunderboltTest *tt, gconstpointer user_data) { g_object_unref (tt->plugin); g_object_unref (tt->bed); g_object_unref (tt->udev_client); if (tt->tree) mock_tree_free (tt->tree); if (tt->fw_data) g_bytes_unref (tt->fw_data); if (tt->fw_file) g_mapped_file_unref (tt->fw_file); } static gboolean test_tree_uuids (const MockTree *node, gpointer data) { const MockTree *root = (MockTree *) data; const gchar *uuid = node->uuid; const MockTree *found; g_assert_nonnull (uuid); g_debug ("Looking for %s", uuid); found = mock_tree_find_uuid (root, uuid); g_assert_nonnull (node); g_assert_nonnull (found); g_assert_cmpstr (node->uuid, ==, found->uuid); /* return false so we traverse the whole tree */ return FALSE; } static void test_tree (ThunderboltTest *tt, gconstpointer user_data) { const MockTree *found; gboolean ret; g_autoptr(MockTree) tree = NULL; tree = mock_tree_init (&root_one); g_assert_nonnull (tree); mock_tree_dump (tree, 0); (void) mock_tree_contains (tree, test_tree_uuids, tree); found = mock_tree_find_uuid (tree, "nonexistentuuid"); g_assert_null (found); ret = mock_tree_attach (tree, tt->bed, tt->plugin); g_assert_true (ret); mock_tree_detach (tree); ret = mock_tree_all (tree, mock_tree_node_is_detached, NULL); g_assert_true (ret); } static gboolean _compare_images (FuThunderboltFirmware *firmware_old, FuThunderboltFirmware *firmware, GError **error) { if (fu_thunderbolt_firmware_is_host (firmware) != fu_thunderbolt_firmware_is_host (firmware_old)) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect firmware mode, got %s, expected %s", fu_thunderbolt_firmware_is_host (firmware) ? "host" : "device", fu_thunderbolt_firmware_is_host (firmware_old) ? "host" : "device"); return FALSE; } if (fu_thunderbolt_firmware_get_vendor_id (firmware) != fu_thunderbolt_firmware_get_vendor_id (firmware_old)) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect device vendor, got 0x%04x, expected 0x%04x", fu_thunderbolt_firmware_get_vendor_id (firmware), fu_thunderbolt_firmware_get_vendor_id (firmware_old)); return FALSE; } if (fu_thunderbolt_firmware_get_device_id (firmware) != fu_thunderbolt_firmware_get_device_id (firmware_old)) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect device type, got 0x%04x, expected 0x%04x", fu_thunderbolt_firmware_get_device_id (firmware), fu_thunderbolt_firmware_get_device_id (firmware_old)); return FALSE; } if (fu_thunderbolt_firmware_get_model_id (firmware) != fu_thunderbolt_firmware_get_model_id (firmware_old)) { g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect device model, got 0x%04x, expected 0x%04x", fu_thunderbolt_firmware_get_model_id (firmware), fu_thunderbolt_firmware_get_model_id (firmware_old)); return FALSE; } if (fu_thunderbolt_firmware_get_has_pd (firmware_old) && !fu_thunderbolt_firmware_get_has_pd (firmware)) { g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "new firmware is missing PD"); return FALSE; } if (fu_thunderbolt_firmware_get_flash_size (firmware) != fu_thunderbolt_firmware_get_flash_size (firmware_old)) { g_set_error_literal (error, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE, "incorrect flash size"); return FALSE; } return TRUE; } static void test_image_validation (ThunderboltTest *tt, gconstpointer user_data) { gboolean ret; g_autofree gchar *ctl_path = NULL; g_autofree gchar *fwi_path = NULL; g_autofree gchar *bad_path = NULL; g_autoptr(GMappedFile) fwi_file = NULL; g_autoptr(GMappedFile) ctl_file = NULL; g_autoptr(GMappedFile) bad_file = NULL; g_autoptr(GBytes) fwi_data = NULL; g_autoptr(GBytes) ctl_data = NULL; g_autoptr(GBytes) bad_data = NULL; g_autoptr(GError) error = NULL; g_autoptr(FuThunderboltFirmwareUpdate) firmware_fwi = fu_thunderbolt_firmware_update_new (); g_autoptr(FuThunderboltFirmware) firmware_ctl = fu_thunderbolt_firmware_new (); g_autoptr(FuThunderboltFirmware) firmware_bad = fu_thunderbolt_firmware_new (); /* image as if read from the controller (i.e. no headers) */ ctl_path = g_build_filename (TESTDATADIR, "thunderbolt/minimal-fw-controller.bin", NULL); ctl_file = g_mapped_file_new (ctl_path, FALSE, &error); g_assert_no_error (error); g_assert_nonnull (ctl_file); ctl_data = g_mapped_file_get_bytes (ctl_file); g_assert_nonnull (ctl_data); /* parse controller image */ ret = fu_firmware_parse (FU_FIRMWARE (firmware_ctl), ctl_data, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_true (ret); g_assert_no_error (error); /* valid firmware update image */ fwi_path = g_build_filename (TESTDATADIR, "thunderbolt/minimal-fw.bin", NULL); fwi_file = g_mapped_file_new (fwi_path, FALSE, &error); g_assert_no_error (error); g_assert_nonnull (fwi_file); fwi_data = g_mapped_file_get_bytes (fwi_file); g_assert_nonnull (fwi_data); /* parse */ ret = fu_firmware_parse (FU_FIRMWARE (firmware_fwi), fwi_data, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_true (ret); g_assert_no_error (error); /* a wrong/bad firmware update image */ bad_path = g_build_filename (TESTDATADIR, "colorhug/firmware.bin", NULL); bad_file = g_mapped_file_new (bad_path, FALSE, &error); g_assert_no_error (error); g_assert_nonnull (bad_file); bad_data = g_mapped_file_get_bytes (bad_file); g_assert_nonnull (bad_data); /* parse; should fail, bad image */ ret = fu_firmware_parse (FU_FIRMWARE (firmware_bad), bad_data, FWUPD_INSTALL_FLAG_NONE, &error); g_assert_false (ret); g_assert_error (error, FWUPD_ERROR, FWUPD_ERROR_READ); g_debug ("expected image validation error [ctl]: %s", error->message); g_clear_error (&error); /* now for some testing ... this should work */ ret = _compare_images (firmware_ctl, FU_THUNDERBOLT_FIRMWARE (firmware_fwi), &error); g_assert_no_error (error); g_assert_true (ret); } static void test_change_uevent (ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; gboolean ret; const gchar *version_after; /* test sanity check */ g_assert_nonnull (tree); /* simulate change of version via a change even, i.e. * without add, remove. */ umockdev_testbed_set_attribute (tt->bed, tree->path, "nvm_version", "42.23"); umockdev_testbed_uevent (tt->bed, tree->path, "change"); /* we just "wait" for 500ms, should be enough */ mock_tree_sync (tree, plugin, 500); /* the tree should not have changed */ ret = mock_tree_all (tree, mock_tree_node_have_fu_device, NULL); g_assert_true (ret); /* we should have the version change in the FuDevice */ version_after = fu_device_get_version (tree->fu_device); g_assert_cmpstr (version_after, ==, "42.23"); } static void test_update_working (ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; GBytes *fw_data = tt->fw_data; gboolean ret; const gchar *version_after; g_autoptr(GError) error = NULL; g_autoptr(UpdateContext) up_ctx = NULL; /* test sanity check */ g_assert_nonnull (tree); g_assert_nonnull (fw_data); /* simulate an update, where the device goes away and comes back * after the time in the last parameter (given in ms) */ up_ctx = mock_tree_prepare_for_update (tree, plugin, "42.23", fw_data, 1000); ret = fu_plugin_runner_update (plugin, tree->fu_device, fw_data, 0, &error); g_assert_no_error (error); g_assert_true (ret); /* we wait until the plugin has picked up all the * subtree changes */ ret = mock_tree_settle (tree, plugin); g_assert_true (ret); ret = fu_plugin_runner_update_attach (plugin, tree->fu_device, &error); g_assert_no_error (error); g_assert_true (ret); version_after = fu_device_get_version (tree->fu_device); g_debug ("version after update: %s", version_after); g_assert_cmpstr (version_after, ==, "42.23"); /* make sure all pending events have happened */ ret = mock_tree_settle (tree, plugin); g_assert_true (ret); /* now we check if the every tree node has a corresponding FuDevice, * this implicitly checks that we are handling uevents correctly * after the event, and that we are in sync with the udev tree */ ret = mock_tree_all (tree, mock_tree_node_have_fu_device, NULL); g_assert_true (ret); } static void test_update_wd19 (ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; GBytes *fw_data = tt->fw_data; gboolean ret; const gchar *version_before; const gchar *version_after; g_autoptr(GError) error = NULL; /* test sanity check */ g_assert_nonnull (tree); g_assert_nonnull (fw_data); /* simulate a wd19 update which will not disappear / re-appear */ fu_device_add_flag (tree->fu_device, FWUPD_DEVICE_FLAG_SKIPS_RESTART); fu_device_add_flag (tree->fu_device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE); version_before = fu_device_get_version (tree->fu_device); ret = fu_plugin_runner_update (plugin, tree->fu_device, fw_data, 0, &error); g_assert_no_error (error); g_assert_true (ret); ret = fu_device_has_flag (tree->fu_device, FWUPD_DEVICE_FLAG_NEEDS_ACTIVATION); g_assert_true (ret); version_after = fu_device_get_version (tree->fu_device); g_assert_cmpstr (version_after, ==, version_before); } static void test_update_fail (ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; GBytes *fw_data = tt->fw_data; gboolean ret; const gchar *version_after; g_autoptr(GError) error = NULL; g_autoptr(UpdateContext) up_ctx = NULL; /* test sanity check */ g_assert_nonnull (tree); g_assert_nonnull (fw_data); /* simulate an update, as in test_update_working, * but simulate an error indicated by the device */ up_ctx = mock_tree_prepare_for_update (tree, plugin, "42.23", fw_data, 1000); up_ctx->result = UPDATE_FAIL_DEVICE_INTERNAL; ret = fu_plugin_runner_update (plugin, tree->fu_device, fw_data, 0, &error); g_assert_no_error (error); g_assert_true (ret); /* we wait until the plugin has picked up all the * subtree changes, and make sure we still receive * udev updates correctly and are in sync */ ret = mock_tree_settle (tree, plugin); g_assert_true (ret); ret = fu_plugin_runner_update_attach (plugin, tree->fu_device, &error); g_assert_error (error, FWUPD_ERROR, FWUPD_ERROR_INTERNAL); g_assert_false (ret); /* make sure all pending events have happened */ ret = mock_tree_settle (tree, plugin); g_assert_true (ret); /* version should *not* have changed (but we get parsed version) */ version_after = fu_device_get_version (tree->fu_device); g_debug ("version after update: %s", version_after); g_assert_cmpstr (version_after, ==, tree->device->nvm_parsed_version); ret = mock_tree_all (tree, mock_tree_node_have_fu_device, NULL); g_assert_true (ret); } static void test_update_fail_nowshow (ThunderboltTest *tt, gconstpointer user_data) { FuPlugin *plugin = tt->plugin; MockTree *tree = tt->tree; GBytes *fw_data = tt->fw_data; gboolean ret; g_autoptr(GError) error = NULL; g_autoptr(UpdateContext) up_ctx = NULL; /* test sanity check */ g_assert_nonnull (tree); g_assert_nonnull (fw_data); /* simulate an update, as in test_update_working, * but simulate an error indicated by the device */ up_ctx = mock_tree_prepare_for_update (tree, plugin, "42.23", fw_data, 1000); up_ctx->result = UPDATE_FAIL_DEVICE_NOSHOW; ret = fu_plugin_runner_update (plugin, tree->fu_device, fw_data, 0, &error); g_assert_no_error (error); g_assert_true (ret); mock_tree_sync (tree, plugin, 500); ret = mock_tree_all (tree, mock_tree_node_have_fu_device, NULL); g_assert_false (ret); } int main (int argc, char **argv) { g_test_init (&argc, &argv, NULL); g_log_set_fatal_mask (NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); g_test_add ("/thunderbolt/basic", ThunderboltTest, NULL, test_set_up, test_tree, test_tear_down); g_test_add ("/thunderbolt/image-validation", ThunderboltTest, TEST_INIT_NONE, test_set_up, test_image_validation, test_tear_down); g_test_add ("/thunderbolt/change-uevent", ThunderboltTest, GUINT_TO_POINTER (TEST_INITIALIZE_TREE | TEST_ATTACH), test_set_up, test_change_uevent, test_tear_down); g_test_add ("/thunderbolt/update{working}", ThunderboltTest, TEST_INIT_FULL, test_set_up, test_update_working, test_tear_down); g_test_add ("/thunderbolt/update{failing}", ThunderboltTest, TEST_INIT_FULL, test_set_up, test_update_fail, test_tear_down); g_test_add ("/thunderbolt/update{failing-noshow}", ThunderboltTest, TEST_INIT_FULL, test_set_up, test_update_fail_nowshow, test_tear_down); g_test_add ("/thunderbolt/update{delayed_activation}", ThunderboltTest, TEST_INIT_FULL, test_set_up, test_update_wd19, test_tear_down); return g_test_run (); }