fwupd/plugins/thunderbolt/fu-self-test.c
2021-08-24 11:18:40 -05:00

1390 lines
35 KiB
C

/*
* Copyright (C) 2017 Christian J. Kellner <christian@kellner.me>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <fcntl.h>
#include <fwupd.h>
#include <glib.h>
#include <glib/gprintf.h>
#include <glib/gstdio.h>
#include <gudev/gudev.h>
#include <locale.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <umockdev.h>
#include <unistd.h>
#include "fu-context-private.h"
#include "fu-plugin-private.h"
#include "fu-thunderbolt-firmware-update.h"
#include "fu-thunderbolt-firmware.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_write_firmware(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_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_write_firmware(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_write_firmware(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_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_write_firmware(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();
}