Allows to reuse code for emulating a character device. It will be used for Smardcard test. Signed-off-by: Frediano Ziglio <fziglio@xxxxxxxxxx> Acked-by: Victor Toso <victortoso@xxxxxxxxxx> --- server/tests/Makefile.am | 2 + server/tests/meson.build | 2 + server/tests/test-stream-device.c | 224 +++++++++--------------------- server/tests/vmc-emu.c | 121 ++++++++++++++++ server/tests/vmc-emu.h | 48 +++++++ 5 files changed, 236 insertions(+), 161 deletions(-) create mode 100644 server/tests/vmc-emu.c create mode 100644 server/tests/vmc-emu.h diff --git a/server/tests/Makefile.am b/server/tests/Makefile.am index 1e62557a8..982508510 100644 --- a/server/tests/Makefile.am +++ b/server/tests/Makefile.am @@ -37,6 +37,8 @@ libtest_a_SOURCES = \ test-glib-compat.h \ win-alarm.c \ win-alarm.h \ + vmc-emu.c \ + vmc-emu.h \ $(NULL) LDADD = \ diff --git a/server/tests/meson.build b/server/tests/meson.build index c9377f1eb..33472f14e 100644 --- a/server/tests/meson.build +++ b/server/tests/meson.build @@ -13,6 +13,8 @@ test_lib_sources = [ 'test-glib-compat.h', 'win-alarm.c', 'win-alarm.h', + 'vmc-emu.c', + 'vmc-emu.h', ] test_libs = [] diff --git a/server/tests/test-stream-device.c b/server/tests/test-stream-device.c index a5e85f118..abf66f043 100644 --- a/server/tests/test-stream-device.c +++ b/server/tests/test-stream-device.c @@ -34,84 +34,9 @@ #include "stream-channel.h" #include "reds.h" #include "win-alarm.h" +#include "vmc-emu.h" -static SpiceCharDeviceInstance vmc_instance; - -// device buffer to read from -static uint8_t message[2048]; -// position to read from -static unsigned pos; -// array of limits when the read should return -// the array is defined as [message_sizes_curr, message_sizes_end) -// then the size is reach we move on next one till exausted -static unsigned message_sizes[16]; -static unsigned *message_sizes_end, *message_sizes_curr; -static bool device_enabled = false; - -static unsigned vmc_write_pos; -static uint8_t vmc_write_buf[2048]; - -// handle writes to the device -static int vmc_write(SPICE_GNUC_UNUSED SpiceCharDeviceInstance *sin, - SPICE_GNUC_UNUSED const uint8_t *buf, - int len) -{ - // just copy into the buffer - unsigned copy = MIN(sizeof(vmc_write_buf) - vmc_write_pos, len); - memcpy(vmc_write_buf+vmc_write_pos, buf, copy); - vmc_write_pos += copy; - return len; -} - -static int vmc_read(SPICE_GNUC_UNUSED SpiceCharDeviceInstance *sin, - uint8_t *buf, - int len) -{ - int ret; - - if (pos >= *message_sizes_curr && message_sizes_curr < message_sizes_end) { - ++message_sizes_curr; - } - if (message_sizes_curr >= message_sizes_end || pos >= *message_sizes_curr) { - return 0; - } - ret = MIN(*message_sizes_curr - pos, len); - memcpy(buf, &message[pos], ret); - pos += ret; - // kick off next message read - // currently Qemu kicks the device so we need to do it manually - // here. If not all data are read, the device goes into blocking - // state and we get the wake only when we read from the device - // again - if (pos >= *message_sizes_curr) { - spice_server_char_device_wakeup(&vmc_instance); - } - return ret; -} - -static void vmc_state(SPICE_GNUC_UNUSED SpiceCharDeviceInstance *sin, - SPICE_GNUC_UNUSED int connected) -{ - device_enabled = !!connected; -} - -static SpiceCharDeviceInterface vmc_interface = { - .base = { - .type = SPICE_INTERFACE_CHAR_DEVICE, - .description = "test spice virtual channel char device", - .major_version = SPICE_INTERFACE_CHAR_DEVICE_MAJOR, - .minor_version = SPICE_INTERFACE_CHAR_DEVICE_MINOR, - }, - .state = vmc_state, - .write = vmc_write, - .read = vmc_read, -}; - -// this specifically creates a stream device -static SpiceCharDeviceInstance vmc_instance = { - .subtype = "port", - .portname = "org.spice-space.stream.0", -}; +static VmcEmu *vmc; static uint8_t *add_stream_hdr(uint8_t *p, StreamMsgType type, uint32_t size) { @@ -145,18 +70,18 @@ discard_server_capabilities(void) { StreamDevHeader hdr; - if (vmc_write_pos == 0) { + if (vmc->write_pos == 0) { return; } - g_assert(vmc_write_pos >= sizeof(hdr)); + g_assert(vmc->write_pos >= sizeof(hdr)); - memcpy(&hdr, vmc_write_buf, sizeof(hdr)); + memcpy(&hdr, vmc->write_buf, sizeof(hdr)); hdr.type = GUINT16_FROM_LE(hdr.type); hdr.size = GUINT32_FROM_LE(hdr.size); if (hdr.type == STREAM_TYPE_CAPABILITIES) { - g_assert_cmpint(hdr.size, <=, vmc_write_pos - sizeof(hdr)); - vmc_write_pos -= hdr.size + sizeof(hdr); - memmove(vmc_write_buf, vmc_write_buf + hdr.size + sizeof(hdr), vmc_write_pos); + g_assert_cmpint(hdr.size, <=, vmc->write_pos - sizeof(hdr)); + vmc->write_pos -= hdr.size + sizeof(hdr); + memmove(vmc->write_buf, vmc->write_buf + hdr.size + sizeof(hdr), vmc->write_pos); } } @@ -168,12 +93,12 @@ check_vmc_error_message(void) discard_server_capabilities(); - g_assert_cmpint(vmc_write_pos, >= ,sizeof(hdr)); + g_assert_cmpint(vmc->write_pos, >= ,sizeof(hdr)); - memcpy(&hdr, vmc_write_buf, sizeof(hdr)); + memcpy(&hdr, vmc->write_buf, sizeof(hdr)); g_assert_cmpint(hdr.protocol_version, ==, STREAM_DEVICE_PROTOCOL); g_assert_cmpint(GUINT16_FROM_LE(hdr.type), ==, STREAM_TYPE_NOTIFY_ERROR); - g_assert_cmpint(GUINT32_FROM_LE(hdr.size), <=, vmc_write_pos - sizeof(hdr)); + g_assert_cmpint(GUINT32_FROM_LE(hdr.size), <=, vmc->write_pos - sizeof(hdr)); } static int num_send_data_calls = 0; @@ -249,15 +174,13 @@ static void test_stream_device_setup(TestFixture *fixture, gconstpointer user_da { g_assert_null(core); g_assert_null(test); + g_assert_null(vmc); core = basic_event_loop_init(); g_assert_nonnull(core); test = test_new(core); g_assert_nonnull(test); - - pos = 0; - vmc_write_pos = 0; - message_sizes_curr = message_sizes; - message_sizes_end = message_sizes; + vmc = vmc_emu_new("port", "org.spice-space.stream.0"); + g_assert_nonnull(vmc); num_send_data_calls = 0; send_data_bytes = 0; @@ -268,6 +191,8 @@ static void test_stream_device_teardown(TestFixture *fixture, gconstpointer user g_assert_nonnull(core); g_assert_nonnull(test); + vmc_emu_destroy(vmc); + vmc = NULL; test_destroy(test); test = NULL; basic_event_loop_destroy(); @@ -276,122 +201,109 @@ static void test_stream_device_teardown(TestFixture *fixture, gconstpointer user static void test_kick(void) { - vmc_instance.base.sif = &vmc_interface.base; - spice_server_add_interface(test->server, &vmc_instance.base); + spice_server_add_interface(test->server, &vmc->instance.base); // we need to open the device and kick the start // the alarm is to prevent the program from getting stuck alarm(5); - spice_server_port_event(&vmc_instance, SPICE_PORT_EVENT_OPENED); - spice_server_char_device_wakeup(&vmc_instance); + spice_server_port_event(&vmc->instance, SPICE_PORT_EVENT_OPENED); + spice_server_char_device_wakeup(&vmc->instance); alarm(0); } static void test_stream_device(TestFixture *fixture, gconstpointer user_data) { - uint8_t *p = message; - for (int test_num=0; test_num < 2; ++test_num) { - pos = 0; - vmc_write_pos = 0; - message_sizes_curr = message_sizes; - message_sizes_end = message_sizes; + vmc_emu_reset(vmc); + uint8_t *p = vmc->message; // add some messages into device buffer // here we are testing the device is reading at least two // consecutive format messages // first message part has 2 extra bytes to check for header split p = add_format(p, 640, 480, SPICE_VIDEO_CODEC_TYPE_MJPEG); - *message_sizes_end = p - message + 2; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, p + 2); p = add_format(p, 640, 480, SPICE_VIDEO_CODEC_TYPE_VP9); // this split the second format in half - *message_sizes_end = p - message - 4; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, p - 4); - *message_sizes_end = p - message; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, p); // add a message to stop data to be read p = add_stream_hdr(p, STREAM_TYPE_INVALID, 0); - *message_sizes_end = p - message; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, p); // this message should not be read p = add_stream_hdr(p, STREAM_TYPE_INVALID, 0); - *message_sizes_end = p - message; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, p); - vmc_instance.base.sif = &vmc_interface.base; - spice_server_add_interface(test->server, &vmc_instance.base); + spice_server_add_interface(test->server, &vmc->instance.base); // device should not have read data before we open it - spice_server_char_device_wakeup(&vmc_instance); - g_assert_cmpint(pos, ==, 0); + spice_server_char_device_wakeup(&vmc->instance); + g_assert_cmpint(vmc->pos, ==, 0); g_test_expect_message(G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "Stream device received invalid message: Invalid message type"); // we need to open the device and kick the start - spice_server_port_event(&vmc_instance, SPICE_PORT_EVENT_OPENED); - spice_server_char_device_wakeup(&vmc_instance); - spice_server_port_event(&vmc_instance, SPICE_PORT_EVENT_CLOSED); + spice_server_port_event(&vmc->instance, SPICE_PORT_EVENT_OPENED); + spice_server_char_device_wakeup(&vmc->instance); + spice_server_port_event(&vmc->instance, SPICE_PORT_EVENT_CLOSED); // make sure first 3 parts are read completely - g_assert(message_sizes_curr - message_sizes >= 3); + g_assert(vmc->message_sizes_curr - vmc->message_sizes >= 3); // make sure the device readed all or that device was // disabled, we need this to make sure that device will be in // sync when opened again - g_assert(message_sizes_curr - message_sizes == 5 || !device_enabled); + g_assert(vmc->message_sizes_curr - vmc->message_sizes == 5 || !vmc->device_enabled); check_vmc_error_message(); - spice_server_remove_interface(&vmc_instance.base); + spice_server_remove_interface(&vmc->instance.base); } } // check if sending a partial message causes issues static void test_stream_device_unfinished(TestFixture *fixture, gconstpointer user_data) { - uint8_t *p = message; + uint8_t *p = vmc->message; // this long and not finished message should not cause an infinite loop p = add_stream_hdr(p, STREAM_TYPE_DATA, 100000); - *message_sizes_end = p - message; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, p); test_kick(); // we should have read all data - g_assert(message_sizes_curr - message_sizes == 1); + g_assert(vmc->message_sizes_curr - vmc->message_sizes == 1); // we should have no data from the device discard_server_capabilities(); - g_assert_cmpint(vmc_write_pos, ==, 0); + g_assert_cmpint(vmc->write_pos, ==, 0); } // check if sending multiple messages cause stall static void test_stream_device_multiple(TestFixture *fixture, gconstpointer user_data) { - uint8_t *p = message; + uint8_t *p = vmc->message; // add some messages into device buffer p = add_format(p, 640, 480, SPICE_VIDEO_CODEC_TYPE_MJPEG); p = add_format(p, 640, 480, SPICE_VIDEO_CODEC_TYPE_MJPEG); p = add_format(p, 640, 480, SPICE_VIDEO_CODEC_TYPE_MJPEG); - *message_sizes_end = p - message; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, p); test_kick(); // we should have read all data - g_assert(message_sizes_curr - message_sizes == 1); + g_assert(vmc->message_sizes_curr - vmc->message_sizes == 1); } // check if data message consume even following message static void test_stream_device_format_after_data(TestFixture *fixture, gconstpointer user_data) { - uint8_t *p = message; + uint8_t *p = vmc->message; // add some messages into device buffer p = add_format(p, 640, 480, SPICE_VIDEO_CODEC_TYPE_MJPEG); @@ -399,15 +311,14 @@ static void test_stream_device_format_after_data(TestFixture *fixture, gconstpoi memcpy(p, "hello", 5); p += 5; p = add_stream_hdr(p, STREAM_TYPE_INVALID, 0); - *message_sizes_end = p - message; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, p); g_test_expect_message(G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "Stream device received invalid message: Invalid message type"); test_kick(); // we should read all data - g_assert(message_sizes_curr - message_sizes == 1); + g_assert(vmc->message_sizes_curr - vmc->message_sizes == 1); // we should have an error back check_vmc_error_message(); @@ -417,46 +328,42 @@ static void test_stream_device_format_after_data(TestFixture *fixture, gconstpoi static void test_stream_device_empty(TestFixture *fixture, gconstpointer user_data) { const StreamMsgType msg_type = (StreamMsgType) GPOINTER_TO_INT(user_data); - uint8_t *p = message; + uint8_t *p = vmc->message; // add some messages into device buffer p = add_stream_hdr(p, msg_type, 0); - *message_sizes_end = p - message; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, p); p = add_format(p, 640, 480, SPICE_VIDEO_CODEC_TYPE_MJPEG); - *message_sizes_end = p - message; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, p); p = add_format(p, 640, 480, SPICE_VIDEO_CODEC_TYPE_MJPEG); - *message_sizes_end = p - message; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, p); test_kick(); // we should read all data - g_assert(message_sizes_curr - message_sizes == 3); + g_assert(vmc->message_sizes_curr - vmc->message_sizes == 3); // we should have no data from the device discard_server_capabilities(); - g_assert_cmpint(vmc_write_pos, ==, 0); + g_assert_cmpint(vmc->write_pos, ==, 0); } // check that server refuse huge data messages static void test_stream_device_huge_data(TestFixture *fixture, gconstpointer user_data) { - uint8_t *p = message; + uint8_t *p = vmc->message; // add some messages into device buffer p = add_stream_hdr(p, STREAM_TYPE_DATA, 33 * 1024 * 1024); p = add_format(p, 640, 480, SPICE_VIDEO_CODEC_TYPE_MJPEG); - *message_sizes_end = p - message; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, p); g_test_expect_message(G_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "Stream device received invalid message: STREAM_DATA too large"); test_kick(); // we should read all data - g_assert(message_sizes_curr - message_sizes == 1); + g_assert(vmc->message_sizes_curr - vmc->message_sizes == 1); // we should have an error back check_vmc_error_message(); @@ -465,7 +372,7 @@ static void test_stream_device_huge_data(TestFixture *fixture, gconstpointer use // check that server send all message static void test_stream_device_data_message(TestFixture *fixture, gconstpointer user_data) { - uint8_t *p = message; + uint8_t *p = vmc->message; // add some messages into device buffer p = add_format(p, 640, 480, SPICE_VIDEO_CODEC_TYPE_MJPEG); @@ -473,23 +380,19 @@ static void test_stream_device_data_message(TestFixture *fixture, gconstpointer for (int i = 0; i < 1017; ++i, ++p) { *p = (uint8_t) (i * 123 + 57); } - *message_sizes_end = 51; - ++message_sizes_end; - *message_sizes_end = 123; - ++message_sizes_end; - *message_sizes_end = 534; - ++message_sizes_end; - *message_sizes_end = p - message; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, vmc->message + 51); + vmc_emu_add_read_till(vmc, vmc->message + 123); + vmc_emu_add_read_till(vmc, vmc->message + 534); + vmc_emu_add_read_till(vmc, p); test_kick(); // we should read all data - g_assert(message_sizes_curr - message_sizes == 4); + g_assert(vmc->message_sizes_curr - vmc->message_sizes == 4); // we should have no data from the device discard_server_capabilities(); - g_assert_cmpint(vmc_write_pos, ==, 0); + g_assert_cmpint(vmc->write_pos, ==, 0); // make sure data were collapsed in a single message g_assert_cmpint(num_send_data_calls, ==, 1); @@ -512,15 +415,14 @@ static void test_display_info(TestFixture *fixture, gconstpointer user_data) .device_display_id = GUINT32_TO_LE(0x0a0b0c0d), .device_address_len = GUINT32_TO_LE(sizeof(address)), }; - uint8_t *p = message; + uint8_t *p = vmc->message; p = add_stream_hdr(p, STREAM_TYPE_DEVICE_DISPLAY_INFO, sizeof(info) + sizeof(address)); memcpy(p, &info, sizeof(info)); p += sizeof(info); strcpy((char*)p, address); p += sizeof(address); - *message_sizes_end = p - message; - ++message_sizes_end; + vmc_emu_add_read_till(vmc, p); // parse the simulated display info message from the stream device so the server now has display // info for the mock stream device diff --git a/server/tests/vmc-emu.c b/server/tests/vmc-emu.c new file mode 100644 index 000000000..418a02139 --- /dev/null +++ b/server/tests/vmc-emu.c @@ -0,0 +1,121 @@ +/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (C) 2019 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ + +#include <config.h> +#include <glib.h> + +#include "vmc-emu.h" + +// handle writes to the device +static int vmc_write(SpiceCharDeviceInstance *sin, + const uint8_t *buf, int len) +{ + VmcEmu *const vmc = SPICE_CONTAINEROF(sin, VmcEmu, instance); + + // just copy into the buffer + unsigned copy = MIN(sizeof(vmc->write_buf) - vmc->write_pos, len); + memcpy(vmc->write_buf+vmc->write_pos, buf, copy); + vmc->write_pos += copy; + return len; +} + +static int vmc_read(SpiceCharDeviceInstance *sin, + uint8_t *buf, int len) +{ + VmcEmu *const vmc = SPICE_CONTAINEROF(sin, VmcEmu, instance); + int ret; + + if (vmc->pos >= *vmc->message_sizes_curr && vmc->message_sizes_curr < vmc->message_sizes_end) { + ++vmc->message_sizes_curr; + } + if (vmc->message_sizes_curr >= vmc->message_sizes_end || vmc->pos >= *vmc->message_sizes_curr) { + return 0; + } + ret = MIN(*vmc->message_sizes_curr - vmc->pos, len); + memcpy(buf, &vmc->message[vmc->pos], ret); + vmc->pos += ret; + // kick off next message read + // currently Qemu kicks the device so we need to do it manually + // here. If not all data are read, the device goes into blocking + // state and we get the wake only when we read from the device + // again + if (vmc->pos >= *vmc->message_sizes_curr) { + spice_server_char_device_wakeup(&vmc->instance); + } + return ret; +} + +static void vmc_state(SpiceCharDeviceInstance *sin, + int connected) +{ + VmcEmu *const vmc = SPICE_CONTAINEROF(sin, VmcEmu, instance); + vmc->device_enabled = !!connected; +} + +static const SpiceCharDeviceInterface vmc_interface = { + .base = { + .type = SPICE_INTERFACE_CHAR_DEVICE, + .description = "test spice virtual channel char device", + .major_version = SPICE_INTERFACE_CHAR_DEVICE_MAJOR, + .minor_version = SPICE_INTERFACE_CHAR_DEVICE_MINOR, + }, + .state = vmc_state, + .write = vmc_write, + .read = vmc_read, +}; + +VmcEmu *vmc_emu_new(const char *subtype, const char *portname) +{ + VmcEmu *vmc = g_new0(VmcEmu, 1); + vmc->vmc_interface = vmc_interface; + vmc->instance.base.sif = &vmc->vmc_interface.base; + vmc->instance.subtype = g_strdup(subtype); + if (portname) { + vmc->instance.portname = g_strdup(portname); + } + vmc_emu_reset(vmc); + return vmc; +} + +void vmc_emu_destroy(VmcEmu *vmc) +{ + g_free((char *) vmc->instance.portname); + g_free((char *) vmc->instance.subtype); + g_free(vmc); +} + +void vmc_emu_reset(VmcEmu *vmc) +{ + vmc->pos = 0; + vmc->write_pos = 0; + vmc->message_sizes_curr = vmc->message_sizes; + vmc->message_sizes_end = vmc->message_sizes; +} + +void vmc_emu_add_read_till(VmcEmu *vmc, uint8_t *end) +{ + g_assert(vmc->message_sizes_end - vmc->message_sizes < G_N_ELEMENTS(vmc->message_sizes)); + g_assert(end >= vmc->message); + g_assert(end - vmc->message <= G_N_ELEMENTS(vmc->message)); + unsigned prev_size = + vmc->message_sizes_end > vmc->message_sizes ? vmc->message_sizes_end[-1] : 0; + unsigned size = end - vmc->message; + g_assert(size >= prev_size); + *vmc->message_sizes_end = size; + ++vmc->message_sizes_end; +} diff --git a/server/tests/vmc-emu.h b/server/tests/vmc-emu.h new file mode 100644 index 000000000..7c26938eb --- /dev/null +++ b/server/tests/vmc-emu.h @@ -0,0 +1,48 @@ +/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (C) 2019 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include "char-device.h" + +typedef struct VmcEmu VmcEmu; + +struct VmcEmu { + SpiceCharDeviceInterface vmc_interface; + SpiceCharDeviceInstance instance; + + // device buffer to read from + uint8_t message[2048]; + // position to read from + unsigned pos; + + // array of limits when the read should return + // the array is defined as [message_sizes_curr, message_sizes_end) + // then the size is reach we move on next one till exausted + unsigned message_sizes[16]; + unsigned *message_sizes_end, *message_sizes_curr; + + bool device_enabled; + + unsigned write_pos; + uint8_t write_buf[2048]; +}; + +VmcEmu *vmc_emu_new(const char *subtype, const char *portname); +void vmc_emu_destroy(VmcEmu *vmc); +void vmc_emu_reset(VmcEmu *vmc); +void vmc_emu_add_read_till(VmcEmu *vmc, uint8_t *end); -- 2.21.0 _______________________________________________ Spice-devel mailing list Spice-devel@xxxxxxxxxxxxxxxxxxxxx https://lists.freedesktop.org/mailman/listinfo/spice-devel