From nobody Tue Jun 23 03:06:50 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 8EC32C433EF for ; Fri, 11 Mar 2022 16:26:06 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1346625AbiCKQ1I (ORCPT ); Fri, 11 Mar 2022 11:27:08 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:38332 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350343AbiCKQ0g (ORCPT ); Fri, 11 Mar 2022 11:26:36 -0500 Received: from smtp1.axis.com (smtp1.axis.com [195.60.68.17]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 388FD1B7637; Fri, 11 Mar 2022 08:24:59 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015900; x=1678551900; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=8onS2RC9Wtu51RueANf2GNphm06S3PZ9X5o0fB0h0Y4=; b=CzCtNMFZOStZGgISMxzXYEAsvGyIWb0TSd9EoMkoNck6JG6ueT9jHHCv rCGSa3uz4bg36ZBEFQS8c8rsMg7AGleQgbTZsFB9NsLk8RR7QRGyzMXoP FzsxQuBw0XVnAz4yq3BfxvkYXcVC9iTt95D/sah1YL66GqGFTHoElXQ9Z PoOnwRskCuajdX4Op9B69ff5SbCUdz7wT/Zn9KmH8QwlNonDkN1BqpW+s S/q8HwrOEjh4DCuRGMf45utPMAo5fN4h0Ql+cHrlUig0cgMUzM0usjmY3 v0zdP0KN8FiNI/5UoauhAz2K6i01Qf5n1KRW9vEwN2rYh3l5AskKaZ8ZT g==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 01/10] roadtest: import libvhost-user from QEMU Date: Fri, 11 Mar 2022 17:24:36 +0100 Message-ID: <20220311162445.346685-2-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Import the libvhost-user from QEMU for use in the implementation of the virtio devices in the roadtest backend. The files are from the following paths in v6.2.0 of QEMU: include/qemu/atomic.h subprojects/libvhost-user/libvhost-user.c subprojects/libvhost-user/libvhost-user.h In order to keep further imports strightforward, the files have been copied as-is with only the following essential modifications: - Addition of SPDX lines (GPL-2.0-or-later, in conformance with the existing copyright messages in the files) - Change of "standard-headers/linux/virtio_ring.h" to in libvhost-user.h. It will be built with the headers from the current kernel. Signed-off-by: Vincent Whitchurch --- .../src/libvhost-user/include/atomic.h | 310 ++ .../src/libvhost-user/libvhost-user.c | 2885 +++++++++++++++++ .../src/libvhost-user/libvhost-user.h | 691 ++++ 3 files changed, 3886 insertions(+) create mode 100644 tools/testing/roadtest/src/libvhost-user/include/atomic= .h create mode 100644 tools/testing/roadtest/src/libvhost-user/libvhost-user.c create mode 100644 tools/testing/roadtest/src/libvhost-user/libvhost-user.h diff --git a/tools/testing/roadtest/src/libvhost-user/include/atomic.h b/to= ols/testing/roadtest/src/libvhost-user/include/atomic.h new file mode 100644 index 000000000000..65026bf54406 --- /dev/null +++ b/tools/testing/roadtest/src/libvhost-user/include/atomic.h @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Simple interface for atomic operations. + * + * Copyright (C) 2013 Red Hat, Inc. + * + * Author: Paolo Bonzini + * + * This work is licensed under the terms of the GNU GPL, version 2 or late= r. + * See the COPYING file in the top-level directory. + * + * See docs/devel/atomics.rst for discussion about the guarantees each + * atomic primitive is meant to provide. + */ + +#ifndef QEMU_ATOMIC_H +#define QEMU_ATOMIC_H + +/* Compiler barrier */ +#define barrier() ({ asm volatile("" ::: "memory"); (void)0; }) + +/* The variable that receives the old value of an atomically-accessed + * variable must be non-qualified, because atomic builtins return values + * through a pointer-type argument as in __atomic_load(&var, &old, MODEL). + * + * This macro has to handle types smaller than int manually, because of + * implicit promotion. int and larger types, as well as pointers, can be + * converted to a non-qualified type just by applying a binary operator. + */ +#define typeof_strip_qual(expr) = \ + typeof( = \ + __builtin_choose_expr( = \ + __builtin_types_compatible_p(typeof(expr), bool) || = \ + __builtin_types_compatible_p(typeof(expr), const bool) || = \ + __builtin_types_compatible_p(typeof(expr), volatile bool) || = \ + __builtin_types_compatible_p(typeof(expr), const volatile bool), = \ + (bool)1, = \ + __builtin_choose_expr( = \ + __builtin_types_compatible_p(typeof(expr), signed char) || = \ + __builtin_types_compatible_p(typeof(expr), const signed char) || = \ + __builtin_types_compatible_p(typeof(expr), volatile signed char) |= | \ + __builtin_types_compatible_p(typeof(expr), const volatile signed c= har), \ + (signed char)1, = \ + __builtin_choose_expr( = \ + __builtin_types_compatible_p(typeof(expr), unsigned char) || = \ + __builtin_types_compatible_p(typeof(expr), const unsigned char) ||= \ + __builtin_types_compatible_p(typeof(expr), volatile unsigned char)= || \ + __builtin_types_compatible_p(typeof(expr), const volatile unsigned= char), \ + (unsigned char)1, = \ + __builtin_choose_expr( = \ + __builtin_types_compatible_p(typeof(expr), signed short) || = \ + __builtin_types_compatible_p(typeof(expr), const signed short) || = \ + __builtin_types_compatible_p(typeof(expr), volatile signed short) = || \ + __builtin_types_compatible_p(typeof(expr), const volatile signed s= hort), \ + (signed short)1, = \ + __builtin_choose_expr( = \ + __builtin_types_compatible_p(typeof(expr), unsigned short) || = \ + __builtin_types_compatible_p(typeof(expr), const unsigned short) |= | \ + __builtin_types_compatible_p(typeof(expr), volatile unsigned short= ) || \ + __builtin_types_compatible_p(typeof(expr), const volatile unsigned= short), \ + (unsigned short)1, = \ + (expr)+0)))))) + +#ifndef __ATOMIC_RELAXED +#error "Expecting C11 atomic ops" +#endif + +/* Manual memory barriers + * + *__atomic_thread_fence does not include a compiler barrier; instead, + * the barrier is part of __atomic_load/__atomic_store's "volatile-like" + * semantics. If smp_wmb() is a no-op, absence of the barrier means that + * the compiler is free to reorder stores on each side of the barrier. + * Add one here, and similarly in smp_rmb() and smp_read_barrier_depends(). + */ + +#define smp_mb() ({ barrier(); __atomic_thread_fence(_= _ATOMIC_SEQ_CST); }) +#define smp_mb_release() ({ barrier(); __atomic_thread_fence(_= _ATOMIC_RELEASE); }) +#define smp_mb_acquire() ({ barrier(); __atomic_thread_fence(_= _ATOMIC_ACQUIRE); }) + +/* Most compilers currently treat consume and acquire the same, but really + * no processors except Alpha need a barrier here. Leave it in if + * using Thread Sanitizer to avoid warnings, otherwise optimize it away. + */ +#if defined(__SANITIZE_THREAD__) +#define smp_read_barrier_depends() ({ barrier(); __atomic_thread_fence(_= _ATOMIC_CONSUME); }) +#elif defined(__alpha__) +#define smp_read_barrier_depends() asm volatile("mb":::"memory") +#else +#define smp_read_barrier_depends() barrier() +#endif + +/* + * A signal barrier forces all pending local memory ops to be observed bef= ore + * a SIGSEGV is delivered to the *same* thread. In practice this is exact= ly + * the same as barrier(), but since we have the correct builtin, use it. + */ +#define signal_barrier() __atomic_signal_fence(__ATOMIC_SEQ_CST) + +/* Sanity check that the size of an atomic operation isn't "overly large". + * Despite the fact that e.g. i686 has 64-bit atomic operations, we do not + * want to use them because we ought not need them, and this lets us do a + * bit of sanity checking that other 32-bit hosts might build. + * + * That said, we have a problem on 64-bit ILP32 hosts in that in order to + * sync with TCG_OVERSIZED_GUEST, this must match TCG_TARGET_REG_BITS. + * We'd prefer not want to pull in everything else TCG related, so handle + * those few cases by hand. + * + * Note that x32 is fully detected with __x86_64__ + _ILP32, and that for + * Sparc we always force the use of sparcv9 in configure. MIPS n32 (ILP32)= & + * n64 (LP64) ABIs are both detected using __mips64. + */ +#if defined(__x86_64__) || defined(__sparc__) || defined(__mips64) +# define ATOMIC_REG_SIZE 8 +#else +# define ATOMIC_REG_SIZE sizeof(void *) +#endif + +/* Weak atomic operations prevent the compiler moving other + * loads/stores past the atomic operation load/store. However there is + * no explicit memory barrier for the processor. + * + * The C11 memory model says that variables that are accessed from + * different threads should at least be done with __ATOMIC_RELAXED + * primitives or the result is undefined. Generally this has little to + * no effect on the generated code but not using the atomic primitives + * will get flagged by sanitizers as a violation. + */ +#define qatomic_read__nocheck(ptr) \ + __atomic_load_n(ptr, __ATOMIC_RELAXED) + +#define qatomic_read(ptr) \ + ({ \ + QEMU_BUILD_BUG_ON(sizeof(*ptr) > ATOMIC_REG_SIZE); \ + qatomic_read__nocheck(ptr); \ + }) + +#define qatomic_set__nocheck(ptr, i) \ + __atomic_store_n(ptr, i, __ATOMIC_RELAXED) + +#define qatomic_set(ptr, i) do { \ + QEMU_BUILD_BUG_ON(sizeof(*ptr) > ATOMIC_REG_SIZE); \ + qatomic_set__nocheck(ptr, i); \ +} while(0) + +/* See above: most compilers currently treat consume and acquire the + * same, but this slows down qatomic_rcu_read unnecessarily. + */ +#ifdef __SANITIZE_THREAD__ +#define qatomic_rcu_read__nocheck(ptr, valptr) \ + __atomic_load(ptr, valptr, __ATOMIC_CONSUME); +#else +#define qatomic_rcu_read__nocheck(ptr, valptr) \ + __atomic_load(ptr, valptr, __ATOMIC_RELAXED); \ + smp_read_barrier_depends(); +#endif + +#define qatomic_rcu_read(ptr) \ + ({ \ + QEMU_BUILD_BUG_ON(sizeof(*ptr) > ATOMIC_REG_SIZE); \ + typeof_strip_qual(*ptr) _val; \ + qatomic_rcu_read__nocheck(ptr, &_val); \ + _val; \ + }) + +#define qatomic_rcu_set(ptr, i) do { \ + QEMU_BUILD_BUG_ON(sizeof(*ptr) > ATOMIC_REG_SIZE); \ + __atomic_store_n(ptr, i, __ATOMIC_RELEASE); \ +} while(0) + +#define qatomic_load_acquire(ptr) \ + ({ \ + QEMU_BUILD_BUG_ON(sizeof(*ptr) > ATOMIC_REG_SIZE); \ + typeof_strip_qual(*ptr) _val; \ + __atomic_load(ptr, &_val, __ATOMIC_ACQUIRE); \ + _val; \ + }) + +#define qatomic_store_release(ptr, i) do { \ + QEMU_BUILD_BUG_ON(sizeof(*ptr) > ATOMIC_REG_SIZE); \ + __atomic_store_n(ptr, i, __ATOMIC_RELEASE); \ +} while(0) + + +/* All the remaining operations are fully sequentially consistent */ + +#define qatomic_xchg__nocheck(ptr, i) ({ \ + __atomic_exchange_n(ptr, (i), __ATOMIC_SEQ_CST); \ +}) + +#define qatomic_xchg(ptr, i) ({ \ + QEMU_BUILD_BUG_ON(sizeof(*ptr) > ATOMIC_REG_SIZE); \ + qatomic_xchg__nocheck(ptr, i); \ +}) + +/* Returns the eventual value, failed or not */ +#define qatomic_cmpxchg__nocheck(ptr, old, new) ({ \ + typeof_strip_qual(*ptr) _old =3D (old); \ + (void)__atomic_compare_exchange_n(ptr, &_old, new, false, \ + __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); \ + _old; \ +}) + +#define qatomic_cmpxchg(ptr, old, new) ({ \ + QEMU_BUILD_BUG_ON(sizeof(*ptr) > ATOMIC_REG_SIZE); \ + qatomic_cmpxchg__nocheck(ptr, old, new); \ +}) + +/* Provide shorter names for GCC atomic builtins, return old value */ +#define qatomic_fetch_inc(ptr) __atomic_fetch_add(ptr, 1, __ATOMIC_SEQ_CS= T) +#define qatomic_fetch_dec(ptr) __atomic_fetch_sub(ptr, 1, __ATOMIC_SEQ_CS= T) + +#define qatomic_fetch_add(ptr, n) __atomic_fetch_add(ptr, n, __ATOMIC_SEQ_= CST) +#define qatomic_fetch_sub(ptr, n) __atomic_fetch_sub(ptr, n, __ATOMIC_SEQ_= CST) +#define qatomic_fetch_and(ptr, n) __atomic_fetch_and(ptr, n, __ATOMIC_SEQ_= CST) +#define qatomic_fetch_or(ptr, n) __atomic_fetch_or(ptr, n, __ATOMIC_SEQ_C= ST) +#define qatomic_fetch_xor(ptr, n) __atomic_fetch_xor(ptr, n, __ATOMIC_SEQ_= CST) + +#define qatomic_inc_fetch(ptr) __atomic_add_fetch(ptr, 1, __ATOMIC_SEQ_= CST) +#define qatomic_dec_fetch(ptr) __atomic_sub_fetch(ptr, 1, __ATOMIC_SEQ_= CST) +#define qatomic_add_fetch(ptr, n) __atomic_add_fetch(ptr, n, __ATOMIC_SEQ_= CST) +#define qatomic_sub_fetch(ptr, n) __atomic_sub_fetch(ptr, n, __ATOMIC_SEQ_= CST) +#define qatomic_and_fetch(ptr, n) __atomic_and_fetch(ptr, n, __ATOMIC_SEQ_= CST) +#define qatomic_or_fetch(ptr, n) __atomic_or_fetch(ptr, n, __ATOMIC_SEQ_C= ST) +#define qatomic_xor_fetch(ptr, n) __atomic_xor_fetch(ptr, n, __ATOMIC_SEQ_= CST) + +/* And even shorter names that return void. */ +#define qatomic_inc(ptr) \ + ((void) __atomic_fetch_add(ptr, 1, __ATOMIC_SEQ_CST)) +#define qatomic_dec(ptr) \ + ((void) __atomic_fetch_sub(ptr, 1, __ATOMIC_SEQ_CST)) +#define qatomic_add(ptr, n) \ + ((void) __atomic_fetch_add(ptr, n, __ATOMIC_SEQ_CST)) +#define qatomic_sub(ptr, n) \ + ((void) __atomic_fetch_sub(ptr, n, __ATOMIC_SEQ_CST)) +#define qatomic_and(ptr, n) \ + ((void) __atomic_fetch_and(ptr, n, __ATOMIC_SEQ_CST)) +#define qatomic_or(ptr, n) \ + ((void) __atomic_fetch_or(ptr, n, __ATOMIC_SEQ_CST)) +#define qatomic_xor(ptr, n) \ + ((void) __atomic_fetch_xor(ptr, n, __ATOMIC_SEQ_CST)) + +#define smp_wmb() smp_mb_release() +#define smp_rmb() smp_mb_acquire() + +/* qatomic_mb_read/set semantics map Java volatile variables. They are + * less expensive on some platforms (notably POWER) than fully + * sequentially consistent operations. + * + * As long as they are used as paired operations they are safe to + * use. See docs/devel/atomics.rst for more discussion. + */ + +#define qatomic_mb_read(ptr) \ + qatomic_load_acquire(ptr) + +#if !defined(__SANITIZE_THREAD__) && \ + (defined(__i386__) || defined(__x86_64__) || defined(__s390x__)) +/* This is more efficient than a store plus a fence. */ +# define qatomic_mb_set(ptr, i) ((void)qatomic_xchg(ptr, i)) +#else +# define qatomic_mb_set(ptr, i) \ + ({ qatomic_store_release(ptr, i); smp_mb(); }) +#endif + +#define qatomic_fetch_inc_nonzero(ptr) ({ \ + typeof_strip_qual(*ptr) _oldn =3D qatomic_read(ptr); \ + while (_oldn && qatomic_cmpxchg(ptr, _oldn, _oldn + 1) !=3D _oldn) { \ + _oldn =3D qatomic_read(ptr); \ + } \ + _oldn; \ +}) + +/* + * Abstractions to access atomically (i.e. "once") i64/u64 variables. + * + * The i386 abi is odd in that by default members are only aligned to + * 4 bytes, which means that 8-byte types can wind up mis-aligned. + * Clang will then warn about this, and emit a call into libatomic. + * + * Use of these types in structures when they will be used with atomic + * operations can avoid this. + */ +typedef int64_t aligned_int64_t __attribute__((aligned(8))); +typedef uint64_t aligned_uint64_t __attribute__((aligned(8))); + +#ifdef CONFIG_ATOMIC64 +/* Use __nocheck because sizeof(void *) might be < sizeof(u64) */ +#define qatomic_read_i64(P) \ + _Generic(*(P), int64_t: qatomic_read__nocheck(P)) +#define qatomic_read_u64(P) \ + _Generic(*(P), uint64_t: qatomic_read__nocheck(P)) +#define qatomic_set_i64(P, V) \ + _Generic(*(P), int64_t: qatomic_set__nocheck(P, V)) +#define qatomic_set_u64(P, V) \ + _Generic(*(P), uint64_t: qatomic_set__nocheck(P, V)) + +static inline void qatomic64_init(void) +{ +} +#else /* !CONFIG_ATOMIC64 */ +int64_t qatomic_read_i64(const int64_t *ptr); +uint64_t qatomic_read_u64(const uint64_t *ptr); +void qatomic_set_i64(int64_t *ptr, int64_t val); +void qatomic_set_u64(uint64_t *ptr, uint64_t val); +void qatomic64_init(void); +#endif /* !CONFIG_ATOMIC64 */ + +#endif /* QEMU_ATOMIC_H */ diff --git a/tools/testing/roadtest/src/libvhost-user/libvhost-user.c b/too= ls/testing/roadtest/src/libvhost-user/libvhost-user.c new file mode 100644 index 000000000000..1e6dddfb10df --- /dev/null +++ b/tools/testing/roadtest/src/libvhost-user/libvhost-user.c @@ -0,0 +1,2885 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Vhost User library + * + * Copyright IBM, Corp. 2007 + * Copyright (c) 2016 Red Hat, Inc. + * + * Authors: + * Anthony Liguori + * Marc-Andr=C3=A9 Lureau + * Victor Kaplansky + * + * This work is licensed under the terms of the GNU GPL, version 2 or + * later. See the COPYING file in the top-level directory. + */ + +/* this code avoids GLib dependency */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#include +#include +#include + +#ifdef __NR_userfaultfd +#include +#endif + +#endif + +#include "include/atomic.h" + +#include "libvhost-user.h" + +/* usually provided by GLib */ +#ifndef MIN +#define MIN(x, y) ({ \ + typeof(x) _min1 =3D (x); \ + typeof(y) _min2 =3D (y); \ + (void) (&_min1 =3D=3D &_min2); \ + _min1 < _min2 ? _min1 : _min2; }) +#endif + +/* Round number down to multiple */ +#define ALIGN_DOWN(n, m) ((n) / (m) * (m)) + +/* Round number up to multiple */ +#define ALIGN_UP(n, m) ALIGN_DOWN((n) + (m) - 1, (m)) + +#ifndef unlikely +#define unlikely(x) __builtin_expect(!!(x), 0) +#endif + +/* Align each region to cache line size in inflight buffer */ +#define INFLIGHT_ALIGNMENT 64 + +/* The version of inflight buffer */ +#define INFLIGHT_VERSION 1 + +/* The version of the protocol we support */ +#define VHOST_USER_VERSION 1 +#define LIBVHOST_USER_DEBUG 0 + +#define DPRINT(...) \ + do { \ + if (LIBVHOST_USER_DEBUG) { \ + fprintf(stderr, __VA_ARGS__); \ + } \ + } while (0) + +static inline +bool has_feature(uint64_t features, unsigned int fbit) +{ + assert(fbit < 64); + return !!(features & (1ULL << fbit)); +} + +static inline +bool vu_has_feature(VuDev *dev, + unsigned int fbit) +{ + return has_feature(dev->features, fbit); +} + +static inline bool vu_has_protocol_feature(VuDev *dev, unsigned int fbit) +{ + return has_feature(dev->protocol_features, fbit); +} + +static const char * +vu_request_to_string(unsigned int req) +{ +#define REQ(req) [req] =3D #req + static const char *vu_request_str[] =3D { + REQ(VHOST_USER_NONE), + REQ(VHOST_USER_GET_FEATURES), + REQ(VHOST_USER_SET_FEATURES), + REQ(VHOST_USER_SET_OWNER), + REQ(VHOST_USER_RESET_OWNER), + REQ(VHOST_USER_SET_MEM_TABLE), + REQ(VHOST_USER_SET_LOG_BASE), + REQ(VHOST_USER_SET_LOG_FD), + REQ(VHOST_USER_SET_VRING_NUM), + REQ(VHOST_USER_SET_VRING_ADDR), + REQ(VHOST_USER_SET_VRING_BASE), + REQ(VHOST_USER_GET_VRING_BASE), + REQ(VHOST_USER_SET_VRING_KICK), + REQ(VHOST_USER_SET_VRING_CALL), + REQ(VHOST_USER_SET_VRING_ERR), + REQ(VHOST_USER_GET_PROTOCOL_FEATURES), + REQ(VHOST_USER_SET_PROTOCOL_FEATURES), + REQ(VHOST_USER_GET_QUEUE_NUM), + REQ(VHOST_USER_SET_VRING_ENABLE), + REQ(VHOST_USER_SEND_RARP), + REQ(VHOST_USER_NET_SET_MTU), + REQ(VHOST_USER_SET_SLAVE_REQ_FD), + REQ(VHOST_USER_IOTLB_MSG), + REQ(VHOST_USER_SET_VRING_ENDIAN), + REQ(VHOST_USER_GET_CONFIG), + REQ(VHOST_USER_SET_CONFIG), + REQ(VHOST_USER_POSTCOPY_ADVISE), + REQ(VHOST_USER_POSTCOPY_LISTEN), + REQ(VHOST_USER_POSTCOPY_END), + REQ(VHOST_USER_GET_INFLIGHT_FD), + REQ(VHOST_USER_SET_INFLIGHT_FD), + REQ(VHOST_USER_GPU_SET_SOCKET), + REQ(VHOST_USER_VRING_KICK), + REQ(VHOST_USER_GET_MAX_MEM_SLOTS), + REQ(VHOST_USER_ADD_MEM_REG), + REQ(VHOST_USER_REM_MEM_REG), + REQ(VHOST_USER_MAX), + }; +#undef REQ + + if (req < VHOST_USER_MAX) { + return vu_request_str[req]; + } else { + return "unknown"; + } +} + +static void +vu_panic(VuDev *dev, const char *msg, ...) +{ + char *buf =3D NULL; + va_list ap; + + va_start(ap, msg); + if (vasprintf(&buf, msg, ap) < 0) { + buf =3D NULL; + } + va_end(ap); + + dev->broken =3D true; + dev->panic(dev, buf); + free(buf); + + /* + * FIXME: + * find a way to call virtio_error, or perhaps close the connection? + */ +} + +/* Translate guest physical address to our virtual address. */ +void * +vu_gpa_to_va(VuDev *dev, uint64_t *plen, uint64_t guest_addr) +{ + int i; + + if (*plen =3D=3D 0) { + return NULL; + } + + /* Find matching memory region. */ + for (i =3D 0; i < dev->nregions; i++) { + VuDevRegion *r =3D &dev->regions[i]; + + if ((guest_addr >=3D r->gpa) && (guest_addr < (r->gpa + r->size)))= { + if ((guest_addr + *plen) > (r->gpa + r->size)) { + *plen =3D r->gpa + r->size - guest_addr; + } + return (void *)(uintptr_t) + guest_addr - r->gpa + r->mmap_addr + r->mmap_offset; + } + } + + return NULL; +} + +/* Translate qemu virtual address to our virtual address. */ +static void * +qva_to_va(VuDev *dev, uint64_t qemu_addr) +{ + int i; + + /* Find matching memory region. */ + for (i =3D 0; i < dev->nregions; i++) { + VuDevRegion *r =3D &dev->regions[i]; + + if ((qemu_addr >=3D r->qva) && (qemu_addr < (r->qva + r->size))) { + return (void *)(uintptr_t) + qemu_addr - r->qva + r->mmap_addr + r->mmap_offset; + } + } + + return NULL; +} + +static void +vmsg_close_fds(VhostUserMsg *vmsg) +{ + int i; + + for (i =3D 0; i < vmsg->fd_num; i++) { + close(vmsg->fds[i]); + } +} + +/* Set reply payload.u64 and clear request flags and fd_num */ +static void vmsg_set_reply_u64(VhostUserMsg *vmsg, uint64_t val) +{ + vmsg->flags =3D 0; /* defaults will be set by vu_send_reply() */ + vmsg->size =3D sizeof(vmsg->payload.u64); + vmsg->payload.u64 =3D val; + vmsg->fd_num =3D 0; +} + +/* A test to see if we have userfault available */ +static bool +have_userfault(void) +{ +#if defined(__linux__) && defined(__NR_userfaultfd) &&\ + defined(UFFD_FEATURE_MISSING_SHMEM) &&\ + defined(UFFD_FEATURE_MISSING_HUGETLBFS) + /* Now test the kernel we're running on really has the features */ + int ufd =3D syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); + struct uffdio_api api_struct; + if (ufd < 0) { + return false; + } + + api_struct.api =3D UFFD_API; + api_struct.features =3D UFFD_FEATURE_MISSING_SHMEM | + UFFD_FEATURE_MISSING_HUGETLBFS; + if (ioctl(ufd, UFFDIO_API, &api_struct)) { + close(ufd); + return false; + } + close(ufd); + return true; + +#else + return false; +#endif +} + +static bool +vu_message_read_default(VuDev *dev, int conn_fd, VhostUserMsg *vmsg) +{ + char control[CMSG_SPACE(VHOST_MEMORY_BASELINE_NREGIONS * sizeof(int))]= =3D {}; + struct iovec iov =3D { + .iov_base =3D (char *)vmsg, + .iov_len =3D VHOST_USER_HDR_SIZE, + }; + struct msghdr msg =3D { + .msg_iov =3D &iov, + .msg_iovlen =3D 1, + .msg_control =3D control, + .msg_controllen =3D sizeof(control), + }; + size_t fd_size; + struct cmsghdr *cmsg; + int rc; + + do { + rc =3D recvmsg(conn_fd, &msg, 0); + } while (rc < 0 && (errno =3D=3D EINTR || errno =3D=3D EAGAIN)); + + if (rc < 0) { + vu_panic(dev, "Error while recvmsg: %s", strerror(errno)); + return false; + } + + vmsg->fd_num =3D 0; + for (cmsg =3D CMSG_FIRSTHDR(&msg); + cmsg !=3D NULL; + cmsg =3D CMSG_NXTHDR(&msg, cmsg)) + { + if (cmsg->cmsg_level =3D=3D SOL_SOCKET && cmsg->cmsg_type =3D=3D S= CM_RIGHTS) { + fd_size =3D cmsg->cmsg_len - CMSG_LEN(0); + vmsg->fd_num =3D fd_size / sizeof(int); + memcpy(vmsg->fds, CMSG_DATA(cmsg), fd_size); + break; + } + } + + if (vmsg->size > sizeof(vmsg->payload)) { + vu_panic(dev, + "Error: too big message request: %d, size: vmsg->size: %u= , " + "while sizeof(vmsg->payload) =3D %zu\n", + vmsg->request, vmsg->size, sizeof(vmsg->payload)); + goto fail; + } + + if (vmsg->size) { + do { + rc =3D read(conn_fd, &vmsg->payload, vmsg->size); + } while (rc < 0 && (errno =3D=3D EINTR || errno =3D=3D EAGAIN)); + + if (rc <=3D 0) { + vu_panic(dev, "Error while reading: %s", strerror(errno)); + goto fail; + } + + assert(rc =3D=3D vmsg->size); + } + + return true; + +fail: + vmsg_close_fds(vmsg); + + return false; +} + +static bool +vu_message_write(VuDev *dev, int conn_fd, VhostUserMsg *vmsg) +{ + int rc; + uint8_t *p =3D (uint8_t *)vmsg; + char control[CMSG_SPACE(VHOST_MEMORY_BASELINE_NREGIONS * sizeof(int))]= =3D {}; + struct iovec iov =3D { + .iov_base =3D (char *)vmsg, + .iov_len =3D VHOST_USER_HDR_SIZE, + }; + struct msghdr msg =3D { + .msg_iov =3D &iov, + .msg_iovlen =3D 1, + .msg_control =3D control, + }; + struct cmsghdr *cmsg; + + memset(control, 0, sizeof(control)); + assert(vmsg->fd_num <=3D VHOST_MEMORY_BASELINE_NREGIONS); + if (vmsg->fd_num > 0) { + size_t fdsize =3D vmsg->fd_num * sizeof(int); + msg.msg_controllen =3D CMSG_SPACE(fdsize); + cmsg =3D CMSG_FIRSTHDR(&msg); + cmsg->cmsg_len =3D CMSG_LEN(fdsize); + cmsg->cmsg_level =3D SOL_SOCKET; + cmsg->cmsg_type =3D SCM_RIGHTS; + memcpy(CMSG_DATA(cmsg), vmsg->fds, fdsize); + } else { + msg.msg_controllen =3D 0; + } + + do { + rc =3D sendmsg(conn_fd, &msg, 0); + } while (rc < 0 && (errno =3D=3D EINTR || errno =3D=3D EAGAIN)); + + if (vmsg->size) { + do { + if (vmsg->data) { + rc =3D write(conn_fd, vmsg->data, vmsg->size); + } else { + rc =3D write(conn_fd, p + VHOST_USER_HDR_SIZE, vmsg->size); + } + } while (rc < 0 && (errno =3D=3D EINTR || errno =3D=3D EAGAIN)); + } + + if (rc <=3D 0) { + vu_panic(dev, "Error while writing: %s", strerror(errno)); + return false; + } + + return true; +} + +static bool +vu_send_reply(VuDev *dev, int conn_fd, VhostUserMsg *vmsg) +{ + /* Set the version in the flags when sending the reply */ + vmsg->flags &=3D ~VHOST_USER_VERSION_MASK; + vmsg->flags |=3D VHOST_USER_VERSION; + vmsg->flags |=3D VHOST_USER_REPLY_MASK; + + return vu_message_write(dev, conn_fd, vmsg); +} + +/* + * Processes a reply on the slave channel. + * Entered with slave_mutex held and releases it before exit. + * Returns true on success. + */ +static bool +vu_process_message_reply(VuDev *dev, const VhostUserMsg *vmsg) +{ + VhostUserMsg msg_reply; + bool result =3D false; + + if ((vmsg->flags & VHOST_USER_NEED_REPLY_MASK) =3D=3D 0) { + result =3D true; + goto out; + } + + if (!vu_message_read_default(dev, dev->slave_fd, &msg_reply)) { + goto out; + } + + if (msg_reply.request !=3D vmsg->request) { + DPRINT("Received unexpected msg type. Expected %d received %d", + vmsg->request, msg_reply.request); + goto out; + } + + result =3D msg_reply.payload.u64 =3D=3D 0; + +out: + pthread_mutex_unlock(&dev->slave_mutex); + return result; +} + +/* Kick the log_call_fd if required. */ +static void +vu_log_kick(VuDev *dev) +{ + if (dev->log_call_fd !=3D -1) { + DPRINT("Kicking the QEMU's log...\n"); + if (eventfd_write(dev->log_call_fd, 1) < 0) { + vu_panic(dev, "Error writing eventfd: %s", strerror(errno)); + } + } +} + +static void +vu_log_page(uint8_t *log_table, uint64_t page) +{ + DPRINT("Logged dirty guest page: %"PRId64"\n", page); + qatomic_or(&log_table[page / 8], 1 << (page % 8)); +} + +static void +vu_log_write(VuDev *dev, uint64_t address, uint64_t length) +{ + uint64_t page; + + if (!(dev->features & (1ULL << VHOST_F_LOG_ALL)) || + !dev->log_table || !length) { + return; + } + + assert(dev->log_size > ((address + length - 1) / VHOST_LOG_PAGE / 8)); + + page =3D address / VHOST_LOG_PAGE; + while (page * VHOST_LOG_PAGE < address + length) { + vu_log_page(dev->log_table, page); + page +=3D 1; + } + + vu_log_kick(dev); +} + +static void +vu_kick_cb(VuDev *dev, int condition, void *data) +{ + int index =3D (intptr_t)data; + VuVirtq *vq =3D &dev->vq[index]; + int sock =3D vq->kick_fd; + eventfd_t kick_data; + ssize_t rc; + + rc =3D eventfd_read(sock, &kick_data); + if (rc =3D=3D -1) { + vu_panic(dev, "kick eventfd_read(): %s", strerror(errno)); + dev->remove_watch(dev, dev->vq[index].kick_fd); + } else { + DPRINT("Got kick_data: %016"PRIx64" handler:%p idx:%d\n", + kick_data, vq->handler, index); + if (vq->handler) { + vq->handler(dev, index); + } + } +} + +static bool +vu_get_features_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + vmsg->payload.u64 =3D + /* + * The following VIRTIO feature bits are supported by our virtqueue + * implementation: + */ + 1ULL << VIRTIO_F_NOTIFY_ON_EMPTY | + 1ULL << VIRTIO_RING_F_INDIRECT_DESC | + 1ULL << VIRTIO_RING_F_EVENT_IDX | + 1ULL << VIRTIO_F_VERSION_1 | + + /* vhost-user feature bits */ + 1ULL << VHOST_F_LOG_ALL | + 1ULL << VHOST_USER_F_PROTOCOL_FEATURES; + + if (dev->iface->get_features) { + vmsg->payload.u64 |=3D dev->iface->get_features(dev); + } + + vmsg->size =3D sizeof(vmsg->payload.u64); + vmsg->fd_num =3D 0; + + DPRINT("Sending back to guest u64: 0x%016"PRIx64"\n", vmsg->payload.u6= 4); + + return true; +} + +static void +vu_set_enable_all_rings(VuDev *dev, bool enabled) +{ + uint16_t i; + + for (i =3D 0; i < dev->max_queues; i++) { + dev->vq[i].enable =3D enabled; + } +} + +static bool +vu_set_features_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + DPRINT("u64: 0x%016"PRIx64"\n", vmsg->payload.u64); + + dev->features =3D vmsg->payload.u64; + if (!vu_has_feature(dev, VIRTIO_F_VERSION_1)) { + /* + * We only support devices conforming to VIRTIO 1.0 or + * later + */ + vu_panic(dev, "virtio legacy devices aren't supported by libvhost-= user"); + return false; + } + + if (!(dev->features & VHOST_USER_F_PROTOCOL_FEATURES)) { + vu_set_enable_all_rings(dev, true); + } + + if (dev->iface->set_features) { + dev->iface->set_features(dev, dev->features); + } + + return false; +} + +static bool +vu_set_owner_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + return false; +} + +static void +vu_close_log(VuDev *dev) +{ + if (dev->log_table) { + if (munmap(dev->log_table, dev->log_size) !=3D 0) { + perror("close log munmap() error"); + } + + dev->log_table =3D NULL; + } + if (dev->log_call_fd !=3D -1) { + close(dev->log_call_fd); + dev->log_call_fd =3D -1; + } +} + +static bool +vu_reset_device_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + vu_set_enable_all_rings(dev, false); + + return false; +} + +static bool +map_ring(VuDev *dev, VuVirtq *vq) +{ + vq->vring.desc =3D qva_to_va(dev, vq->vra.desc_user_addr); + vq->vring.used =3D qva_to_va(dev, vq->vra.used_user_addr); + vq->vring.avail =3D qva_to_va(dev, vq->vra.avail_user_addr); + + DPRINT("Setting virtq addresses:\n"); + DPRINT(" vring_desc at %p\n", vq->vring.desc); + DPRINT(" vring_used at %p\n", vq->vring.used); + DPRINT(" vring_avail at %p\n", vq->vring.avail); + + return !(vq->vring.desc && vq->vring.used && vq->vring.avail); +} + +static bool +generate_faults(VuDev *dev) { + int i; + for (i =3D 0; i < dev->nregions; i++) { + VuDevRegion *dev_region =3D &dev->regions[i]; + int ret; +#ifdef UFFDIO_REGISTER + /* + * We should already have an open ufd. Mark each memory + * range as ufd. + * Discard any mapping we have here; note I can't use MADV_REMOVE + * or fallocate to make the hole since I don't want to lose + * data that's already arrived in the shared process. + * TODO: How to do hugepage + */ + ret =3D madvise((void *)(uintptr_t)dev_region->mmap_addr, + dev_region->size + dev_region->mmap_offset, + MADV_DONTNEED); + if (ret) { + fprintf(stderr, + "%s: Failed to madvise(DONTNEED) region %d: %s\n", + __func__, i, strerror(errno)); + } + /* + * Turn off transparent hugepages so we dont get lose wakeups + * in neighbouring pages. + * TODO: Turn this backon later. + */ + ret =3D madvise((void *)(uintptr_t)dev_region->mmap_addr, + dev_region->size + dev_region->mmap_offset, + MADV_NOHUGEPAGE); + if (ret) { + /* + * Note: This can happen legally on kernels that are configured + * without madvise'able hugepages + */ + fprintf(stderr, + "%s: Failed to madvise(NOHUGEPAGE) region %d: %s\n", + __func__, i, strerror(errno)); + } + struct uffdio_register reg_struct; + reg_struct.range.start =3D (uintptr_t)dev_region->mmap_addr; + reg_struct.range.len =3D dev_region->size + dev_region->mmap_offse= t; + reg_struct.mode =3D UFFDIO_REGISTER_MODE_MISSING; + + if (ioctl(dev->postcopy_ufd, UFFDIO_REGISTER, ®_struct)) { + vu_panic(dev, "%s: Failed to userfault region %d " + "@%p + size:%zx offset: %zx: (ufd=3D%d)%s\n", + __func__, i, + dev_region->mmap_addr, + dev_region->size, dev_region->mmap_offset, + dev->postcopy_ufd, strerror(errno)); + return false; + } + if (!(reg_struct.ioctls & ((__u64)1 << _UFFDIO_COPY))) { + vu_panic(dev, "%s Region (%d) doesn't support COPY", + __func__, i); + return false; + } + DPRINT("%s: region %d: Registered userfault for %" + PRIx64 " + %" PRIx64 "\n", __func__, i, + (uint64_t)reg_struct.range.start, + (uint64_t)reg_struct.range.len); + /* Now it's registered we can let the client at it */ + if (mprotect((void *)(uintptr_t)dev_region->mmap_addr, + dev_region->size + dev_region->mmap_offset, + PROT_READ | PROT_WRITE)) { + vu_panic(dev, "failed to mprotect region %d for postcopy (%s)", + i, strerror(errno)); + return false; + } + /* TODO: Stash 'zero' support flags somewhere */ +#endif + } + + return true; +} + +static bool +vu_add_mem_reg(VuDev *dev, VhostUserMsg *vmsg) { + int i; + bool track_ramblocks =3D dev->postcopy_listening; + VhostUserMemoryRegion m =3D vmsg->payload.memreg.region, *msg_region = =3D &m; + VuDevRegion *dev_region =3D &dev->regions[dev->nregions]; + void *mmap_addr; + + /* + * If we are in postcopy mode and we receive a u64 payload with a 0 va= lue + * we know all the postcopy client bases have been received, and we + * should start generating faults. + */ + if (track_ramblocks && + vmsg->size =3D=3D sizeof(vmsg->payload.u64) && + vmsg->payload.u64 =3D=3D 0) { + (void)generate_faults(dev); + return false; + } + + DPRINT("Adding region: %u\n", dev->nregions); + DPRINT(" guest_phys_addr: 0x%016"PRIx64"\n", + msg_region->guest_phys_addr); + DPRINT(" memory_size: 0x%016"PRIx64"\n", + msg_region->memory_size); + DPRINT(" userspace_addr 0x%016"PRIx64"\n", + msg_region->userspace_addr); + DPRINT(" mmap_offset 0x%016"PRIx64"\n", + msg_region->mmap_offset); + + dev_region->gpa =3D msg_region->guest_phys_addr; + dev_region->size =3D msg_region->memory_size; + dev_region->qva =3D msg_region->userspace_addr; + dev_region->mmap_offset =3D msg_region->mmap_offset; + + /* + * We don't use offset argument of mmap() since the + * mapped address has to be page aligned, and we use huge + * pages. + */ + if (track_ramblocks) { + /* + * In postcopy we're using PROT_NONE here to catch anyone + * accessing it before we userfault. + */ + mmap_addr =3D mmap(0, dev_region->size + dev_region->mmap_offset, + PROT_NONE, MAP_SHARED, + vmsg->fds[0], 0); + } else { + mmap_addr =3D mmap(0, dev_region->size + dev_region->mmap_offset, + PROT_READ | PROT_WRITE, MAP_SHARED, vmsg->fds[0], + 0); + } + + if (mmap_addr =3D=3D MAP_FAILED) { + vu_panic(dev, "region mmap error: %s", strerror(errno)); + } else { + dev_region->mmap_addr =3D (uint64_t)(uintptr_t)mmap_addr; + DPRINT(" mmap_addr: 0x%016"PRIx64"\n", + dev_region->mmap_addr); + } + + close(vmsg->fds[0]); + + if (track_ramblocks) { + /* + * Return the address to QEMU so that it can translate the ufd + * fault addresses back. + */ + msg_region->userspace_addr =3D (uintptr_t)(mmap_addr + + dev_region->mmap_offset); + + /* Send the message back to qemu with the addresses filled in. */ + vmsg->fd_num =3D 0; + if (!vu_send_reply(dev, dev->sock, vmsg)) { + vu_panic(dev, "failed to respond to add-mem-region for postcop= y"); + return false; + } + + DPRINT("Successfully added new region in postcopy\n"); + dev->nregions++; + return false; + + } else { + for (i =3D 0; i < dev->max_queues; i++) { + if (dev->vq[i].vring.desc) { + if (map_ring(dev, &dev->vq[i])) { + vu_panic(dev, "remapping queue %d for new memory regio= n", + i); + } + } + } + + DPRINT("Successfully added new region\n"); + dev->nregions++; + vmsg_set_reply_u64(vmsg, 0); + return true; + } +} + +static inline bool reg_equal(VuDevRegion *vudev_reg, + VhostUserMemoryRegion *msg_reg) +{ + if (vudev_reg->gpa =3D=3D msg_reg->guest_phys_addr && + vudev_reg->qva =3D=3D msg_reg->userspace_addr && + vudev_reg->size =3D=3D msg_reg->memory_size) { + return true; + } + + return false; +} + +static bool +vu_rem_mem_reg(VuDev *dev, VhostUserMsg *vmsg) { + int i, j; + bool found =3D false; + VuDevRegion shadow_regions[VHOST_USER_MAX_RAM_SLOTS] =3D {}; + VhostUserMemoryRegion m =3D vmsg->payload.memreg.region, *msg_region = =3D &m; + + DPRINT("Removing region:\n"); + DPRINT(" guest_phys_addr: 0x%016"PRIx64"\n", + msg_region->guest_phys_addr); + DPRINT(" memory_size: 0x%016"PRIx64"\n", + msg_region->memory_size); + DPRINT(" userspace_addr 0x%016"PRIx64"\n", + msg_region->userspace_addr); + DPRINT(" mmap_offset 0x%016"PRIx64"\n", + msg_region->mmap_offset); + + for (i =3D 0, j =3D 0; i < dev->nregions; i++) { + if (!reg_equal(&dev->regions[i], msg_region)) { + shadow_regions[j].gpa =3D dev->regions[i].gpa; + shadow_regions[j].size =3D dev->regions[i].size; + shadow_regions[j].qva =3D dev->regions[i].qva; + shadow_regions[j].mmap_addr =3D dev->regions[i].mmap_addr; + shadow_regions[j].mmap_offset =3D dev->regions[i].mmap_offset; + j++; + } else { + found =3D true; + VuDevRegion *r =3D &dev->regions[i]; + void *m =3D (void *) (uintptr_t) r->mmap_addr; + + if (m) { + munmap(m, r->size + r->mmap_offset); + } + } + } + + if (found) { + memcpy(dev->regions, shadow_regions, + sizeof(VuDevRegion) * VHOST_USER_MAX_RAM_SLOTS); + DPRINT("Successfully removed a region\n"); + dev->nregions--; + vmsg_set_reply_u64(vmsg, 0); + } else { + vu_panic(dev, "Specified region not found\n"); + } + + return true; +} + +static bool +vu_set_mem_table_exec_postcopy(VuDev *dev, VhostUserMsg *vmsg) +{ + int i; + VhostUserMemory m =3D vmsg->payload.memory, *memory =3D &m; + dev->nregions =3D memory->nregions; + + DPRINT("Nregions: %u\n", memory->nregions); + for (i =3D 0; i < dev->nregions; i++) { + void *mmap_addr; + VhostUserMemoryRegion *msg_region =3D &memory->regions[i]; + VuDevRegion *dev_region =3D &dev->regions[i]; + + DPRINT("Region %d\n", i); + DPRINT(" guest_phys_addr: 0x%016"PRIx64"\n", + msg_region->guest_phys_addr); + DPRINT(" memory_size: 0x%016"PRIx64"\n", + msg_region->memory_size); + DPRINT(" userspace_addr 0x%016"PRIx64"\n", + msg_region->userspace_addr); + DPRINT(" mmap_offset 0x%016"PRIx64"\n", + msg_region->mmap_offset); + + dev_region->gpa =3D msg_region->guest_phys_addr; + dev_region->size =3D msg_region->memory_size; + dev_region->qva =3D msg_region->userspace_addr; + dev_region->mmap_offset =3D msg_region->mmap_offset; + + /* We don't use offset argument of mmap() since the + * mapped address has to be page aligned, and we use huge + * pages. + * In postcopy we're using PROT_NONE here to catch anyone + * accessing it before we userfault + */ + mmap_addr =3D mmap(0, dev_region->size + dev_region->mmap_offset, + PROT_NONE, MAP_SHARED, + vmsg->fds[i], 0); + + if (mmap_addr =3D=3D MAP_FAILED) { + vu_panic(dev, "region mmap error: %s", strerror(errno)); + } else { + dev_region->mmap_addr =3D (uint64_t)(uintptr_t)mmap_addr; + DPRINT(" mmap_addr: 0x%016"PRIx64"\n", + dev_region->mmap_addr); + } + + /* Return the address to QEMU so that it can translate the ufd + * fault addresses back. + */ + msg_region->userspace_addr =3D (uintptr_t)(mmap_addr + + dev_region->mmap_offset); + close(vmsg->fds[i]); + } + + /* Send the message back to qemu with the addresses filled in */ + vmsg->fd_num =3D 0; + if (!vu_send_reply(dev, dev->sock, vmsg)) { + vu_panic(dev, "failed to respond to set-mem-table for postcopy"); + return false; + } + + /* Wait for QEMU to confirm that it's registered the handler for the + * faults. + */ + if (!dev->read_msg(dev, dev->sock, vmsg) || + vmsg->size !=3D sizeof(vmsg->payload.u64) || + vmsg->payload.u64 !=3D 0) { + vu_panic(dev, "failed to receive valid ack for postcopy set-mem-ta= ble"); + return false; + } + + /* OK, now we can go and register the memory and generate faults */ + (void)generate_faults(dev); + + return false; +} + +static bool +vu_set_mem_table_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + int i; + VhostUserMemory m =3D vmsg->payload.memory, *memory =3D &m; + + for (i =3D 0; i < dev->nregions; i++) { + VuDevRegion *r =3D &dev->regions[i]; + void *m =3D (void *) (uintptr_t) r->mmap_addr; + + if (m) { + munmap(m, r->size + r->mmap_offset); + } + } + dev->nregions =3D memory->nregions; + + if (dev->postcopy_listening) { + return vu_set_mem_table_exec_postcopy(dev, vmsg); + } + + DPRINT("Nregions: %u\n", memory->nregions); + for (i =3D 0; i < dev->nregions; i++) { + void *mmap_addr; + VhostUserMemoryRegion *msg_region =3D &memory->regions[i]; + VuDevRegion *dev_region =3D &dev->regions[i]; + + DPRINT("Region %d\n", i); + DPRINT(" guest_phys_addr: 0x%016"PRIx64"\n", + msg_region->guest_phys_addr); + DPRINT(" memory_size: 0x%016"PRIx64"\n", + msg_region->memory_size); + DPRINT(" userspace_addr 0x%016"PRIx64"\n", + msg_region->userspace_addr); + DPRINT(" mmap_offset 0x%016"PRIx64"\n", + msg_region->mmap_offset); + + dev_region->gpa =3D msg_region->guest_phys_addr; + dev_region->size =3D msg_region->memory_size; + dev_region->qva =3D msg_region->userspace_addr; + dev_region->mmap_offset =3D msg_region->mmap_offset; + + /* We don't use offset argument of mmap() since the + * mapped address has to be page aligned, and we use huge + * pages. */ + mmap_addr =3D mmap(0, dev_region->size + dev_region->mmap_offset, + PROT_READ | PROT_WRITE, MAP_SHARED, + vmsg->fds[i], 0); + + if (mmap_addr =3D=3D MAP_FAILED) { + vu_panic(dev, "region mmap error: %s", strerror(errno)); + } else { + dev_region->mmap_addr =3D (uint64_t)(uintptr_t)mmap_addr; + DPRINT(" mmap_addr: 0x%016"PRIx64"\n", + dev_region->mmap_addr); + } + + close(vmsg->fds[i]); + } + + for (i =3D 0; i < dev->max_queues; i++) { + if (dev->vq[i].vring.desc) { + if (map_ring(dev, &dev->vq[i])) { + vu_panic(dev, "remapping queue %d during setmemtable", i); + } + } + } + + return false; +} + +static bool +vu_set_log_base_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + int fd; + uint64_t log_mmap_size, log_mmap_offset; + void *rc; + + if (vmsg->fd_num !=3D 1 || + vmsg->size !=3D sizeof(vmsg->payload.log)) { + vu_panic(dev, "Invalid log_base message"); + return true; + } + + fd =3D vmsg->fds[0]; + log_mmap_offset =3D vmsg->payload.log.mmap_offset; + log_mmap_size =3D vmsg->payload.log.mmap_size; + DPRINT("Log mmap_offset: %"PRId64"\n", log_mmap_offset); + DPRINT("Log mmap_size: %"PRId64"\n", log_mmap_size); + + rc =3D mmap(0, log_mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, + log_mmap_offset); + close(fd); + if (rc =3D=3D MAP_FAILED) { + perror("log mmap error"); + } + + if (dev->log_table) { + munmap(dev->log_table, dev->log_size); + } + dev->log_table =3D rc; + dev->log_size =3D log_mmap_size; + + vmsg->size =3D sizeof(vmsg->payload.u64); + vmsg->fd_num =3D 0; + + return true; +} + +static bool +vu_set_log_fd_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + if (vmsg->fd_num !=3D 1) { + vu_panic(dev, "Invalid log_fd message"); + return false; + } + + if (dev->log_call_fd !=3D -1) { + close(dev->log_call_fd); + } + dev->log_call_fd =3D vmsg->fds[0]; + DPRINT("Got log_call_fd: %d\n", vmsg->fds[0]); + + return false; +} + +static bool +vu_set_vring_num_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + unsigned int index =3D vmsg->payload.state.index; + unsigned int num =3D vmsg->payload.state.num; + + DPRINT("State.index: %u\n", index); + DPRINT("State.num: %u\n", num); + dev->vq[index].vring.num =3D num; + + return false; +} + +static bool +vu_set_vring_addr_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + struct vhost_vring_addr addr =3D vmsg->payload.addr, *vra =3D &addr; + unsigned int index =3D vra->index; + VuVirtq *vq =3D &dev->vq[index]; + + DPRINT("vhost_vring_addr:\n"); + DPRINT(" index: %d\n", vra->index); + DPRINT(" flags: %d\n", vra->flags); + DPRINT(" desc_user_addr: 0x%016" PRIx64 "\n", (uint64_t)vra->desc= _user_addr); + DPRINT(" used_user_addr: 0x%016" PRIx64 "\n", (uint64_t)vra->used= _user_addr); + DPRINT(" avail_user_addr: 0x%016" PRIx64 "\n", (uint64_t)vra->avai= l_user_addr); + DPRINT(" log_guest_addr: 0x%016" PRIx64 "\n", (uint64_t)vra->log_= guest_addr); + + vq->vra =3D *vra; + vq->vring.flags =3D vra->flags; + vq->vring.log_guest_addr =3D vra->log_guest_addr; + + + if (map_ring(dev, vq)) { + vu_panic(dev, "Invalid vring_addr message"); + return false; + } + + vq->used_idx =3D le16toh(vq->vring.used->idx); + + if (vq->last_avail_idx !=3D vq->used_idx) { + bool resume =3D dev->iface->queue_is_processed_in_order && + dev->iface->queue_is_processed_in_order(dev, index); + + DPRINT("Last avail index !=3D used index: %u !=3D %u%s\n", + vq->last_avail_idx, vq->used_idx, + resume ? ", resuming" : ""); + + if (resume) { + vq->shadow_avail_idx =3D vq->last_avail_idx =3D vq->used_idx; + } + } + + return false; +} + +static bool +vu_set_vring_base_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + unsigned int index =3D vmsg->payload.state.index; + unsigned int num =3D vmsg->payload.state.num; + + DPRINT("State.index: %u\n", index); + DPRINT("State.num: %u\n", num); + dev->vq[index].shadow_avail_idx =3D dev->vq[index].last_avail_idx =3D = num; + + return false; +} + +static bool +vu_get_vring_base_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + unsigned int index =3D vmsg->payload.state.index; + + DPRINT("State.index: %u\n", index); + vmsg->payload.state.num =3D dev->vq[index].last_avail_idx; + vmsg->size =3D sizeof(vmsg->payload.state); + + dev->vq[index].started =3D false; + if (dev->iface->queue_set_started) { + dev->iface->queue_set_started(dev, index, false); + } + + if (dev->vq[index].call_fd !=3D -1) { + close(dev->vq[index].call_fd); + dev->vq[index].call_fd =3D -1; + } + if (dev->vq[index].kick_fd !=3D -1) { + dev->remove_watch(dev, dev->vq[index].kick_fd); + close(dev->vq[index].kick_fd); + dev->vq[index].kick_fd =3D -1; + } + + return true; +} + +static bool +vu_check_queue_msg_file(VuDev *dev, VhostUserMsg *vmsg) +{ + int index =3D vmsg->payload.u64 & VHOST_USER_VRING_IDX_MASK; + bool nofd =3D vmsg->payload.u64 & VHOST_USER_VRING_NOFD_MASK; + + if (index >=3D dev->max_queues) { + vmsg_close_fds(vmsg); + vu_panic(dev, "Invalid queue index: %u", index); + return false; + } + + if (nofd) { + vmsg_close_fds(vmsg); + return true; + } + + if (vmsg->fd_num !=3D 1) { + vmsg_close_fds(vmsg); + vu_panic(dev, "Invalid fds in request: %d", vmsg->request); + return false; + } + + return true; +} + +static int +inflight_desc_compare(const void *a, const void *b) +{ + VuVirtqInflightDesc *desc0 =3D (VuVirtqInflightDesc *)a, + *desc1 =3D (VuVirtqInflightDesc *)b; + + if (desc1->counter > desc0->counter && + (desc1->counter - desc0->counter) < VIRTQUEUE_MAX_SIZE * 2) { + return 1; + } + + return -1; +} + +static int +vu_check_queue_inflights(VuDev *dev, VuVirtq *vq) +{ + int i =3D 0; + + if (!vu_has_protocol_feature(dev, VHOST_USER_PROTOCOL_F_INFLIGHT_SHMFD= )) { + return 0; + } + + if (unlikely(!vq->inflight)) { + return -1; + } + + if (unlikely(!vq->inflight->version)) { + /* initialize the buffer */ + vq->inflight->version =3D INFLIGHT_VERSION; + return 0; + } + + vq->used_idx =3D le16toh(vq->vring.used->idx); + vq->resubmit_num =3D 0; + vq->resubmit_list =3D NULL; + vq->counter =3D 0; + + if (unlikely(vq->inflight->used_idx !=3D vq->used_idx)) { + vq->inflight->desc[vq->inflight->last_batch_head].inflight =3D 0; + + barrier(); + + vq->inflight->used_idx =3D vq->used_idx; + } + + for (i =3D 0; i < vq->inflight->desc_num; i++) { + if (vq->inflight->desc[i].inflight =3D=3D 1) { + vq->inuse++; + } + } + + vq->shadow_avail_idx =3D vq->last_avail_idx =3D vq->inuse + vq->used_i= dx; + + if (vq->inuse) { + vq->resubmit_list =3D calloc(vq->inuse, sizeof(VuVirtqInflightDesc= )); + if (!vq->resubmit_list) { + return -1; + } + + for (i =3D 0; i < vq->inflight->desc_num; i++) { + if (vq->inflight->desc[i].inflight) { + vq->resubmit_list[vq->resubmit_num].index =3D i; + vq->resubmit_list[vq->resubmit_num].counter =3D + vq->inflight->desc[i].counter; + vq->resubmit_num++; + } + } + + if (vq->resubmit_num > 1) { + qsort(vq->resubmit_list, vq->resubmit_num, + sizeof(VuVirtqInflightDesc), inflight_desc_compare); + } + vq->counter =3D vq->resubmit_list[0].counter + 1; + } + + /* in case of I/O hang after reconnecting */ + if (eventfd_write(vq->kick_fd, 1)) { + return -1; + } + + return 0; +} + +static bool +vu_set_vring_kick_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + int index =3D vmsg->payload.u64 & VHOST_USER_VRING_IDX_MASK; + bool nofd =3D vmsg->payload.u64 & VHOST_USER_VRING_NOFD_MASK; + + DPRINT("u64: 0x%016"PRIx64"\n", vmsg->payload.u64); + + if (!vu_check_queue_msg_file(dev, vmsg)) { + return false; + } + + if (dev->vq[index].kick_fd !=3D -1) { + dev->remove_watch(dev, dev->vq[index].kick_fd); + close(dev->vq[index].kick_fd); + dev->vq[index].kick_fd =3D -1; + } + + dev->vq[index].kick_fd =3D nofd ? -1 : vmsg->fds[0]; + DPRINT("Got kick_fd: %d for vq: %d\n", dev->vq[index].kick_fd, index); + + dev->vq[index].started =3D true; + if (dev->iface->queue_set_started) { + dev->iface->queue_set_started(dev, index, true); + } + + if (dev->vq[index].kick_fd !=3D -1 && dev->vq[index].handler) { + dev->set_watch(dev, dev->vq[index].kick_fd, VU_WATCH_IN, + vu_kick_cb, (void *)(long)index); + + DPRINT("Waiting for kicks on fd: %d for vq: %d\n", + dev->vq[index].kick_fd, index); + } + + if (vu_check_queue_inflights(dev, &dev->vq[index])) { + vu_panic(dev, "Failed to check inflights for vq: %d\n", index); + } + + return false; +} + +void vu_set_queue_handler(VuDev *dev, VuVirtq *vq, + vu_queue_handler_cb handler) +{ + int qidx =3D vq - dev->vq; + + vq->handler =3D handler; + if (vq->kick_fd >=3D 0) { + if (handler) { + dev->set_watch(dev, vq->kick_fd, VU_WATCH_IN, + vu_kick_cb, (void *)(long)qidx); + } else { + dev->remove_watch(dev, vq->kick_fd); + } + } +} + +bool vu_set_queue_host_notifier(VuDev *dev, VuVirtq *vq, int fd, + int size, int offset) +{ + int qidx =3D vq - dev->vq; + int fd_num =3D 0; + VhostUserMsg vmsg =3D { + .request =3D VHOST_USER_SLAVE_VRING_HOST_NOTIFIER_MSG, + .flags =3D VHOST_USER_VERSION | VHOST_USER_NEED_REPLY_MASK, + .size =3D sizeof(vmsg.payload.area), + .payload.area =3D { + .u64 =3D qidx & VHOST_USER_VRING_IDX_MASK, + .size =3D size, + .offset =3D offset, + }, + }; + + if (fd =3D=3D -1) { + vmsg.payload.area.u64 |=3D VHOST_USER_VRING_NOFD_MASK; + } else { + vmsg.fds[fd_num++] =3D fd; + } + + vmsg.fd_num =3D fd_num; + + if (!vu_has_protocol_feature(dev, VHOST_USER_PROTOCOL_F_SLAVE_SEND_FD)= ) { + return false; + } + + pthread_mutex_lock(&dev->slave_mutex); + if (!vu_message_write(dev, dev->slave_fd, &vmsg)) { + pthread_mutex_unlock(&dev->slave_mutex); + return false; + } + + /* Also unlocks the slave_mutex */ + return vu_process_message_reply(dev, &vmsg); +} + +static bool +vu_set_vring_call_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + int index =3D vmsg->payload.u64 & VHOST_USER_VRING_IDX_MASK; + bool nofd =3D vmsg->payload.u64 & VHOST_USER_VRING_NOFD_MASK; + + DPRINT("u64: 0x%016"PRIx64"\n", vmsg->payload.u64); + + if (!vu_check_queue_msg_file(dev, vmsg)) { + return false; + } + + if (dev->vq[index].call_fd !=3D -1) { + close(dev->vq[index].call_fd); + dev->vq[index].call_fd =3D -1; + } + + dev->vq[index].call_fd =3D nofd ? -1 : vmsg->fds[0]; + + /* in case of I/O hang after reconnecting */ + if (dev->vq[index].call_fd !=3D -1 && eventfd_write(vmsg->fds[0], 1)) { + return -1; + } + + DPRINT("Got call_fd: %d for vq: %d\n", dev->vq[index].call_fd, index); + + return false; +} + +static bool +vu_set_vring_err_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + int index =3D vmsg->payload.u64 & VHOST_USER_VRING_IDX_MASK; + bool nofd =3D vmsg->payload.u64 & VHOST_USER_VRING_NOFD_MASK; + + DPRINT("u64: 0x%016"PRIx64"\n", vmsg->payload.u64); + + if (!vu_check_queue_msg_file(dev, vmsg)) { + return false; + } + + if (dev->vq[index].err_fd !=3D -1) { + close(dev->vq[index].err_fd); + dev->vq[index].err_fd =3D -1; + } + + dev->vq[index].err_fd =3D nofd ? -1 : vmsg->fds[0]; + + return false; +} + +static bool +vu_get_protocol_features_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + /* + * Note that we support, but intentionally do not set, + * VHOST_USER_PROTOCOL_F_INBAND_NOTIFICATIONS. This means that + * a device implementation can return it in its callback + * (get_protocol_features) if it wants to use this for + * simulation, but it is otherwise not desirable (if even + * implemented by the master.) + */ + uint64_t features =3D 1ULL << VHOST_USER_PROTOCOL_F_MQ | + 1ULL << VHOST_USER_PROTOCOL_F_LOG_SHMFD | + 1ULL << VHOST_USER_PROTOCOL_F_SLAVE_REQ | + 1ULL << VHOST_USER_PROTOCOL_F_HOST_NOTIFIER | + 1ULL << VHOST_USER_PROTOCOL_F_SLAVE_SEND_FD | + 1ULL << VHOST_USER_PROTOCOL_F_REPLY_ACK | + 1ULL << VHOST_USER_PROTOCOL_F_CONFIGURE_MEM_SLOTS; + + if (have_userfault()) { + features |=3D 1ULL << VHOST_USER_PROTOCOL_F_PAGEFAULT; + } + + if (dev->iface->get_config && dev->iface->set_config) { + features |=3D 1ULL << VHOST_USER_PROTOCOL_F_CONFIG; + } + + if (dev->iface->get_protocol_features) { + features |=3D dev->iface->get_protocol_features(dev); + } + + vmsg_set_reply_u64(vmsg, features); + return true; +} + +static bool +vu_set_protocol_features_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + uint64_t features =3D vmsg->payload.u64; + + DPRINT("u64: 0x%016"PRIx64"\n", features); + + dev->protocol_features =3D vmsg->payload.u64; + + if (vu_has_protocol_feature(dev, + VHOST_USER_PROTOCOL_F_INBAND_NOTIFICATIONS= ) && + (!vu_has_protocol_feature(dev, VHOST_USER_PROTOCOL_F_SLAVE_REQ) || + !vu_has_protocol_feature(dev, VHOST_USER_PROTOCOL_F_REPLY_ACK))) { + /* + * The use case for using messages for kick/call is simulation, to= make + * the kick and call synchronous. To actually get that behaviour, = both + * of the other features are required. + * Theoretically, one could use only kick messages, or do them wit= hout + * having F_REPLY_ACK, but too many (possibly pending) messages on= the + * socket will eventually cause the master to hang, to avoid this = in + * scenarios where not desired enforce that the settings are in a = way + * that actually enables the simulation case. + */ + vu_panic(dev, + "F_IN_BAND_NOTIFICATIONS requires F_SLAVE_REQ && F_REPLY_= ACK"); + return false; + } + + if (dev->iface->set_protocol_features) { + dev->iface->set_protocol_features(dev, features); + } + + return false; +} + +static bool +vu_get_queue_num_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + vmsg_set_reply_u64(vmsg, dev->max_queues); + return true; +} + +static bool +vu_set_vring_enable_exec(VuDev *dev, VhostUserMsg *vmsg) +{ + unsigned int index =3D vmsg->payload.state.index; + unsigned int enable =3D vmsg->payload.state.num; + + DPRINT("State.index: %u\n", index); + DPRINT("State.enable: %u\n", enable); + + if (index >=3D dev->max_queues) { + vu_panic(dev, "Invalid vring_enable index: %u", index); + return false; + } + + dev->vq[index].enable =3D enable; + return false; +} + +static bool +vu_set_slave_req_fd(VuDev *dev, VhostUserMsg *vmsg) +{ + if (vmsg->fd_num !=3D 1) { + vu_panic(dev, "Invalid slave_req_fd message (%d fd's)", vmsg->fd_n= um); + return false; + } + + if (dev->slave_fd !=3D -1) { + close(dev->slave_fd); + } + dev->slave_fd =3D vmsg->fds[0]; + DPRINT("Got slave_fd: %d\n", vmsg->fds[0]); + + return false; +} + +static bool +vu_get_config(VuDev *dev, VhostUserMsg *vmsg) +{ + int ret =3D -1; + + if (dev->iface->get_config) { + ret =3D dev->iface->get_config(dev, vmsg->payload.config.region, + vmsg->payload.config.size); + } + + if (ret) { + /* resize to zero to indicate an error to master */ + vmsg->size =3D 0; + } + + return true; +} + +static bool +vu_set_config(VuDev *dev, VhostUserMsg *vmsg) +{ + int ret =3D -1; + + if (dev->iface->set_config) { + ret =3D dev->iface->set_config(dev, vmsg->payload.config.region, + vmsg->payload.config.offset, + vmsg->payload.config.size, + vmsg->payload.config.flags); + if (ret) { + vu_panic(dev, "Set virtio configuration space failed"); + } + } + + return false; +} + +static bool +vu_set_postcopy_advise(VuDev *dev, VhostUserMsg *vmsg) +{ + dev->postcopy_ufd =3D -1; +#ifdef UFFDIO_API + struct uffdio_api api_struct; + + dev->postcopy_ufd =3D syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK= ); + vmsg->size =3D 0; +#endif + + if (dev->postcopy_ufd =3D=3D -1) { + vu_panic(dev, "Userfaultfd not available: %s", strerror(errno)); + goto out; + } + +#ifdef UFFDIO_API + api_struct.api =3D UFFD_API; + api_struct.features =3D 0; + if (ioctl(dev->postcopy_ufd, UFFDIO_API, &api_struct)) { + vu_panic(dev, "Failed UFFDIO_API: %s", strerror(errno)); + close(dev->postcopy_ufd); + dev->postcopy_ufd =3D -1; + goto out; + } + /* TODO: Stash feature flags somewhere */ +#endif + +out: + /* Return a ufd to the QEMU */ + vmsg->fd_num =3D 1; + vmsg->fds[0] =3D dev->postcopy_ufd; + return true; /* =3D send a reply */ +} + +static bool +vu_set_postcopy_listen(VuDev *dev, VhostUserMsg *vmsg) +{ + if (dev->nregions) { + vu_panic(dev, "Regions already registered at postcopy-listen"); + vmsg_set_reply_u64(vmsg, -1); + return true; + } + dev->postcopy_listening =3D true; + + vmsg_set_reply_u64(vmsg, 0); + return true; +} + +static bool +vu_set_postcopy_end(VuDev *dev, VhostUserMsg *vmsg) +{ + DPRINT("%s: Entry\n", __func__); + dev->postcopy_listening =3D false; + if (dev->postcopy_ufd > 0) { + close(dev->postcopy_ufd); + dev->postcopy_ufd =3D -1; + DPRINT("%s: Done close\n", __func__); + } + + vmsg_set_reply_u64(vmsg, 0); + DPRINT("%s: exit\n", __func__); + return true; +} + +static inline uint64_t +vu_inflight_queue_size(uint16_t queue_size) +{ + return ALIGN_UP(sizeof(VuDescStateSplit) * queue_size + + sizeof(uint16_t), INFLIGHT_ALIGNMENT); +} + +#ifdef MFD_ALLOW_SEALING +static void * +memfd_alloc(const char *name, size_t size, unsigned int flags, int *fd) +{ + void *ptr; + int ret; + + *fd =3D memfd_create(name, MFD_ALLOW_SEALING); + if (*fd < 0) { + return NULL; + } + + ret =3D ftruncate(*fd, size); + if (ret < 0) { + close(*fd); + return NULL; + } + + ret =3D fcntl(*fd, F_ADD_SEALS, flags); + if (ret < 0) { + close(*fd); + return NULL; + } + + ptr =3D mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED, *fd, 0); + if (ptr =3D=3D MAP_FAILED) { + close(*fd); + return NULL; + } + + return ptr; +} +#endif + +static bool +vu_get_inflight_fd(VuDev *dev, VhostUserMsg *vmsg) +{ + int fd =3D -1; + void *addr =3D NULL; + uint64_t mmap_size; + uint16_t num_queues, queue_size; + + if (vmsg->size !=3D sizeof(vmsg->payload.inflight)) { + vu_panic(dev, "Invalid get_inflight_fd message:%d", vmsg->size); + vmsg->payload.inflight.mmap_size =3D 0; + return true; + } + + num_queues =3D vmsg->payload.inflight.num_queues; + queue_size =3D vmsg->payload.inflight.queue_size; + + DPRINT("set_inflight_fd num_queues: %"PRId16"\n", num_queues); + DPRINT("set_inflight_fd queue_size: %"PRId16"\n", queue_size); + + mmap_size =3D vu_inflight_queue_size(queue_size) * num_queues; + +#ifdef MFD_ALLOW_SEALING + addr =3D memfd_alloc("vhost-inflight", mmap_size, + F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL, + &fd); +#else + vu_panic(dev, "Not implemented: memfd support is missing"); +#endif + + if (!addr) { + vu_panic(dev, "Failed to alloc vhost inflight area"); + vmsg->payload.inflight.mmap_size =3D 0; + return true; + } + + memset(addr, 0, mmap_size); + + dev->inflight_info.addr =3D addr; + dev->inflight_info.size =3D vmsg->payload.inflight.mmap_size =3D mmap_= size; + dev->inflight_info.fd =3D vmsg->fds[0] =3D fd; + vmsg->fd_num =3D 1; + vmsg->payload.inflight.mmap_offset =3D 0; + + DPRINT("send inflight mmap_size: %"PRId64"\n", + vmsg->payload.inflight.mmap_size); + DPRINT("send inflight mmap offset: %"PRId64"\n", + vmsg->payload.inflight.mmap_offset); + + return true; +} + +static bool +vu_set_inflight_fd(VuDev *dev, VhostUserMsg *vmsg) +{ + int fd, i; + uint64_t mmap_size, mmap_offset; + uint16_t num_queues, queue_size; + void *rc; + + if (vmsg->fd_num !=3D 1 || + vmsg->size !=3D sizeof(vmsg->payload.inflight)) { + vu_panic(dev, "Invalid set_inflight_fd message size:%d fds:%d", + vmsg->size, vmsg->fd_num); + return false; + } + + fd =3D vmsg->fds[0]; + mmap_size =3D vmsg->payload.inflight.mmap_size; + mmap_offset =3D vmsg->payload.inflight.mmap_offset; + num_queues =3D vmsg->payload.inflight.num_queues; + queue_size =3D vmsg->payload.inflight.queue_size; + + DPRINT("set_inflight_fd mmap_size: %"PRId64"\n", mmap_size); + DPRINT("set_inflight_fd mmap_offset: %"PRId64"\n", mmap_offset); + DPRINT("set_inflight_fd num_queues: %"PRId16"\n", num_queues); + DPRINT("set_inflight_fd queue_size: %"PRId16"\n", queue_size); + + rc =3D mmap(0, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, + fd, mmap_offset); + + if (rc =3D=3D MAP_FAILED) { + vu_panic(dev, "set_inflight_fd mmap error: %s", strerror(errno)); + return false; + } + + if (dev->inflight_info.fd) { + close(dev->inflight_info.fd); + } + + if (dev->inflight_info.addr) { + munmap(dev->inflight_info.addr, dev->inflight_info.size); + } + + dev->inflight_info.fd =3D fd; + dev->inflight_info.addr =3D rc; + dev->inflight_info.size =3D mmap_size; + + for (i =3D 0; i < num_queues; i++) { + dev->vq[i].inflight =3D (VuVirtqInflight *)rc; + dev->vq[i].inflight->desc_num =3D queue_size; + rc =3D (void *)((char *)rc + vu_inflight_queue_size(queue_size)); + } + + return false; +} + +static bool +vu_handle_vring_kick(VuDev *dev, VhostUserMsg *vmsg) +{ + unsigned int index =3D vmsg->payload.state.index; + + if (index >=3D dev->max_queues) { + vu_panic(dev, "Invalid queue index: %u", index); + return false; + } + + DPRINT("Got kick message: handler:%p idx:%u\n", + dev->vq[index].handler, index); + + if (!dev->vq[index].started) { + dev->vq[index].started =3D true; + + if (dev->iface->queue_set_started) { + dev->iface->queue_set_started(dev, index, true); + } + } + + if (dev->vq[index].handler) { + dev->vq[index].handler(dev, index); + } + + return false; +} + +static bool vu_handle_get_max_memslots(VuDev *dev, VhostUserMsg *vmsg) +{ + vmsg->flags =3D VHOST_USER_REPLY_MASK | VHOST_USER_VERSION; + vmsg->size =3D sizeof(vmsg->payload.u64); + vmsg->payload.u64 =3D VHOST_USER_MAX_RAM_SLOTS; + vmsg->fd_num =3D 0; + + if (!vu_message_write(dev, dev->sock, vmsg)) { + vu_panic(dev, "Failed to send max ram slots: %s\n", strerror(errno= )); + } + + DPRINT("u64: 0x%016"PRIx64"\n", (uint64_t) VHOST_USER_MAX_RAM_SLOTS); + + return false; +} + +static bool +vu_process_message(VuDev *dev, VhostUserMsg *vmsg) +{ + int do_reply =3D 0; + + /* Print out generic part of the request. */ + DPRINT("=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D Vhost user me= ssage =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D\n"); + DPRINT("Request: %s (%d)\n", vu_request_to_string(vmsg->request), + vmsg->request); + DPRINT("Flags: 0x%x\n", vmsg->flags); + DPRINT("Size: %u\n", vmsg->size); + + if (vmsg->fd_num) { + int i; + DPRINT("Fds:"); + for (i =3D 0; i < vmsg->fd_num; i++) { + DPRINT(" %d", vmsg->fds[i]); + } + DPRINT("\n"); + } + + if (dev->iface->process_msg && + dev->iface->process_msg(dev, vmsg, &do_reply)) { + return do_reply; + } + + switch (vmsg->request) { + case VHOST_USER_GET_FEATURES: + return vu_get_features_exec(dev, vmsg); + case VHOST_USER_SET_FEATURES: + return vu_set_features_exec(dev, vmsg); + case VHOST_USER_GET_PROTOCOL_FEATURES: + return vu_get_protocol_features_exec(dev, vmsg); + case VHOST_USER_SET_PROTOCOL_FEATURES: + return vu_set_protocol_features_exec(dev, vmsg); + case VHOST_USER_SET_OWNER: + return vu_set_owner_exec(dev, vmsg); + case VHOST_USER_RESET_OWNER: + return vu_reset_device_exec(dev, vmsg); + case VHOST_USER_SET_MEM_TABLE: + return vu_set_mem_table_exec(dev, vmsg); + case VHOST_USER_SET_LOG_BASE: + return vu_set_log_base_exec(dev, vmsg); + case VHOST_USER_SET_LOG_FD: + return vu_set_log_fd_exec(dev, vmsg); + case VHOST_USER_SET_VRING_NUM: + return vu_set_vring_num_exec(dev, vmsg); + case VHOST_USER_SET_VRING_ADDR: + return vu_set_vring_addr_exec(dev, vmsg); + case VHOST_USER_SET_VRING_BASE: + return vu_set_vring_base_exec(dev, vmsg); + case VHOST_USER_GET_VRING_BASE: + return vu_get_vring_base_exec(dev, vmsg); + case VHOST_USER_SET_VRING_KICK: + return vu_set_vring_kick_exec(dev, vmsg); + case VHOST_USER_SET_VRING_CALL: + return vu_set_vring_call_exec(dev, vmsg); + case VHOST_USER_SET_VRING_ERR: + return vu_set_vring_err_exec(dev, vmsg); + case VHOST_USER_GET_QUEUE_NUM: + return vu_get_queue_num_exec(dev, vmsg); + case VHOST_USER_SET_VRING_ENABLE: + return vu_set_vring_enable_exec(dev, vmsg); + case VHOST_USER_SET_SLAVE_REQ_FD: + return vu_set_slave_req_fd(dev, vmsg); + case VHOST_USER_GET_CONFIG: + return vu_get_config(dev, vmsg); + case VHOST_USER_SET_CONFIG: + return vu_set_config(dev, vmsg); + case VHOST_USER_NONE: + /* if you need processing before exit, override iface->process_msg= */ + exit(0); + case VHOST_USER_POSTCOPY_ADVISE: + return vu_set_postcopy_advise(dev, vmsg); + case VHOST_USER_POSTCOPY_LISTEN: + return vu_set_postcopy_listen(dev, vmsg); + case VHOST_USER_POSTCOPY_END: + return vu_set_postcopy_end(dev, vmsg); + case VHOST_USER_GET_INFLIGHT_FD: + return vu_get_inflight_fd(dev, vmsg); + case VHOST_USER_SET_INFLIGHT_FD: + return vu_set_inflight_fd(dev, vmsg); + case VHOST_USER_VRING_KICK: + return vu_handle_vring_kick(dev, vmsg); + case VHOST_USER_GET_MAX_MEM_SLOTS: + return vu_handle_get_max_memslots(dev, vmsg); + case VHOST_USER_ADD_MEM_REG: + return vu_add_mem_reg(dev, vmsg); + case VHOST_USER_REM_MEM_REG: + return vu_rem_mem_reg(dev, vmsg); + default: + vmsg_close_fds(vmsg); + vu_panic(dev, "Unhandled request: %d", vmsg->request); + } + + return false; +} + +bool +vu_dispatch(VuDev *dev) +{ + VhostUserMsg vmsg =3D { 0, }; + int reply_requested; + bool need_reply, success =3D false; + + if (!dev->read_msg(dev, dev->sock, &vmsg)) { + goto end; + } + + need_reply =3D vmsg.flags & VHOST_USER_NEED_REPLY_MASK; + + reply_requested =3D vu_process_message(dev, &vmsg); + if (!reply_requested && need_reply) { + vmsg_set_reply_u64(&vmsg, 0); + reply_requested =3D 1; + } + + if (!reply_requested) { + success =3D true; + goto end; + } + + if (!vu_send_reply(dev, dev->sock, &vmsg)) { + goto end; + } + + success =3D true; + +end: + free(vmsg.data); + return success; +} + +void +vu_deinit(VuDev *dev) +{ + int i; + + for (i =3D 0; i < dev->nregions; i++) { + VuDevRegion *r =3D &dev->regions[i]; + void *m =3D (void *) (uintptr_t) r->mmap_addr; + if (m !=3D MAP_FAILED) { + munmap(m, r->size + r->mmap_offset); + } + } + dev->nregions =3D 0; + + for (i =3D 0; i < dev->max_queues; i++) { + VuVirtq *vq =3D &dev->vq[i]; + + if (vq->call_fd !=3D -1) { + close(vq->call_fd); + vq->call_fd =3D -1; + } + + if (vq->kick_fd !=3D -1) { + dev->remove_watch(dev, vq->kick_fd); + close(vq->kick_fd); + vq->kick_fd =3D -1; + } + + if (vq->err_fd !=3D -1) { + close(vq->err_fd); + vq->err_fd =3D -1; + } + + if (vq->resubmit_list) { + free(vq->resubmit_list); + vq->resubmit_list =3D NULL; + } + + vq->inflight =3D NULL; + } + + if (dev->inflight_info.addr) { + munmap(dev->inflight_info.addr, dev->inflight_info.size); + dev->inflight_info.addr =3D NULL; + } + + if (dev->inflight_info.fd > 0) { + close(dev->inflight_info.fd); + dev->inflight_info.fd =3D -1; + } + + vu_close_log(dev); + if (dev->slave_fd !=3D -1) { + close(dev->slave_fd); + dev->slave_fd =3D -1; + } + pthread_mutex_destroy(&dev->slave_mutex); + + if (dev->sock !=3D -1) { + close(dev->sock); + } + + free(dev->vq); + dev->vq =3D NULL; +} + +bool +vu_init(VuDev *dev, + uint16_t max_queues, + int socket, + vu_panic_cb panic, + vu_read_msg_cb read_msg, + vu_set_watch_cb set_watch, + vu_remove_watch_cb remove_watch, + const VuDevIface *iface) +{ + uint16_t i; + + assert(max_queues > 0); + assert(socket >=3D 0); + assert(set_watch); + assert(remove_watch); + assert(iface); + assert(panic); + + memset(dev, 0, sizeof(*dev)); + + dev->sock =3D socket; + dev->panic =3D panic; + dev->read_msg =3D read_msg ? read_msg : vu_message_read_default; + dev->set_watch =3D set_watch; + dev->remove_watch =3D remove_watch; + dev->iface =3D iface; + dev->log_call_fd =3D -1; + pthread_mutex_init(&dev->slave_mutex, NULL); + dev->slave_fd =3D -1; + dev->max_queues =3D max_queues; + + dev->vq =3D malloc(max_queues * sizeof(dev->vq[0])); + if (!dev->vq) { + DPRINT("%s: failed to malloc virtqueues\n", __func__); + return false; + } + + for (i =3D 0; i < max_queues; i++) { + dev->vq[i] =3D (VuVirtq) { + .call_fd =3D -1, .kick_fd =3D -1, .err_fd =3D -1, + .notification =3D true, + }; + } + + return true; +} + +VuVirtq * +vu_get_queue(VuDev *dev, int qidx) +{ + assert(qidx < dev->max_queues); + return &dev->vq[qidx]; +} + +bool +vu_queue_enabled(VuDev *dev, VuVirtq *vq) +{ + return vq->enable; +} + +bool +vu_queue_started(const VuDev *dev, const VuVirtq *vq) +{ + return vq->started; +} + +static inline uint16_t +vring_avail_flags(VuVirtq *vq) +{ + return le16toh(vq->vring.avail->flags); +} + +static inline uint16_t +vring_avail_idx(VuVirtq *vq) +{ + vq->shadow_avail_idx =3D le16toh(vq->vring.avail->idx); + + return vq->shadow_avail_idx; +} + +static inline uint16_t +vring_avail_ring(VuVirtq *vq, int i) +{ + return le16toh(vq->vring.avail->ring[i]); +} + +static inline uint16_t +vring_get_used_event(VuVirtq *vq) +{ + return vring_avail_ring(vq, vq->vring.num); +} + +static int +virtqueue_num_heads(VuDev *dev, VuVirtq *vq, unsigned int idx) +{ + uint16_t num_heads =3D vring_avail_idx(vq) - idx; + + /* Check it isn't doing very strange things with descriptor numbers. */ + if (num_heads > vq->vring.num) { + vu_panic(dev, "Guest moved used index from %u to %u", + idx, vq->shadow_avail_idx); + return -1; + } + if (num_heads) { + /* On success, callers read a descriptor at vq->last_avail_idx. + * Make sure descriptor read does not bypass avail index read. */ + smp_rmb(); + } + + return num_heads; +} + +static bool +virtqueue_get_head(VuDev *dev, VuVirtq *vq, + unsigned int idx, unsigned int *head) +{ + /* Grab the next descriptor number they're advertising, and increment + * the index we've seen. */ + *head =3D vring_avail_ring(vq, idx % vq->vring.num); + + /* If their number is silly, that's a fatal mistake. */ + if (*head >=3D vq->vring.num) { + vu_panic(dev, "Guest says index %u is available", *head); + return false; + } + + return true; +} + +static int +virtqueue_read_indirect_desc(VuDev *dev, struct vring_desc *desc, + uint64_t addr, size_t len) +{ + struct vring_desc *ori_desc; + uint64_t read_len; + + if (len > (VIRTQUEUE_MAX_SIZE * sizeof(struct vring_desc))) { + return -1; + } + + if (len =3D=3D 0) { + return -1; + } + + while (len) { + read_len =3D len; + ori_desc =3D vu_gpa_to_va(dev, &read_len, addr); + if (!ori_desc) { + return -1; + } + + memcpy(desc, ori_desc, read_len); + len -=3D read_len; + addr +=3D read_len; + desc +=3D read_len; + } + + return 0; +} + +enum { + VIRTQUEUE_READ_DESC_ERROR =3D -1, + VIRTQUEUE_READ_DESC_DONE =3D 0, /* end of chain */ + VIRTQUEUE_READ_DESC_MORE =3D 1, /* more buffers in chain */ +}; + +static int +virtqueue_read_next_desc(VuDev *dev, struct vring_desc *desc, + int i, unsigned int max, unsigned int *next) +{ + /* If this descriptor says it doesn't chain, we're done. */ + if (!(le16toh(desc[i].flags) & VRING_DESC_F_NEXT)) { + return VIRTQUEUE_READ_DESC_DONE; + } + + /* Check they're not leading us off end of descriptors. */ + *next =3D le16toh(desc[i].next); + /* Make sure compiler knows to grab that: we don't want it changing! */ + smp_wmb(); + + if (*next >=3D max) { + vu_panic(dev, "Desc next is %u", *next); + return VIRTQUEUE_READ_DESC_ERROR; + } + + return VIRTQUEUE_READ_DESC_MORE; +} + +void +vu_queue_get_avail_bytes(VuDev *dev, VuVirtq *vq, unsigned int *in_bytes, + unsigned int *out_bytes, + unsigned max_in_bytes, unsigned max_out_bytes) +{ + unsigned int idx; + unsigned int total_bufs, in_total, out_total; + int rc; + + idx =3D vq->last_avail_idx; + + total_bufs =3D in_total =3D out_total =3D 0; + if (unlikely(dev->broken) || + unlikely(!vq->vring.avail)) { + goto done; + } + + while ((rc =3D virtqueue_num_heads(dev, vq, idx)) > 0) { + unsigned int max, desc_len, num_bufs, indirect =3D 0; + uint64_t desc_addr, read_len; + struct vring_desc *desc; + struct vring_desc desc_buf[VIRTQUEUE_MAX_SIZE]; + unsigned int i; + + max =3D vq->vring.num; + num_bufs =3D total_bufs; + if (!virtqueue_get_head(dev, vq, idx++, &i)) { + goto err; + } + desc =3D vq->vring.desc; + + if (le16toh(desc[i].flags) & VRING_DESC_F_INDIRECT) { + if (le32toh(desc[i].len) % sizeof(struct vring_desc)) { + vu_panic(dev, "Invalid size for indirect buffer table"); + goto err; + } + + /* If we've got too many, that implies a descriptor loop. */ + if (num_bufs >=3D max) { + vu_panic(dev, "Looped descriptor"); + goto err; + } + + /* loop over the indirect descriptor table */ + indirect =3D 1; + desc_addr =3D le64toh(desc[i].addr); + desc_len =3D le32toh(desc[i].len); + max =3D desc_len / sizeof(struct vring_desc); + read_len =3D desc_len; + desc =3D vu_gpa_to_va(dev, &read_len, desc_addr); + if (unlikely(desc && read_len !=3D desc_len)) { + /* Failed to use zero copy */ + desc =3D NULL; + if (!virtqueue_read_indirect_desc(dev, desc_buf, + desc_addr, + desc_len)) { + desc =3D desc_buf; + } + } + if (!desc) { + vu_panic(dev, "Invalid indirect buffer table"); + goto err; + } + num_bufs =3D i =3D 0; + } + + do { + /* If we've got too many, that implies a descriptor loop. */ + if (++num_bufs > max) { + vu_panic(dev, "Looped descriptor"); + goto err; + } + + if (le16toh(desc[i].flags) & VRING_DESC_F_WRITE) { + in_total +=3D le32toh(desc[i].len); + } else { + out_total +=3D le32toh(desc[i].len); + } + if (in_total >=3D max_in_bytes && out_total >=3D max_out_bytes= ) { + goto done; + } + rc =3D virtqueue_read_next_desc(dev, desc, i, max, &i); + } while (rc =3D=3D VIRTQUEUE_READ_DESC_MORE); + + if (rc =3D=3D VIRTQUEUE_READ_DESC_ERROR) { + goto err; + } + + if (!indirect) { + total_bufs =3D num_bufs; + } else { + total_bufs++; + } + } + if (rc < 0) { + goto err; + } +done: + if (in_bytes) { + *in_bytes =3D in_total; + } + if (out_bytes) { + *out_bytes =3D out_total; + } + return; + +err: + in_total =3D out_total =3D 0; + goto done; +} + +bool +vu_queue_avail_bytes(VuDev *dev, VuVirtq *vq, unsigned int in_bytes, + unsigned int out_bytes) +{ + unsigned int in_total, out_total; + + vu_queue_get_avail_bytes(dev, vq, &in_total, &out_total, + in_bytes, out_bytes); + + return in_bytes <=3D in_total && out_bytes <=3D out_total; +} + +/* Fetch avail_idx from VQ memory only when we really need to know if + * guest has added some buffers. */ +bool +vu_queue_empty(VuDev *dev, VuVirtq *vq) +{ + if (unlikely(dev->broken) || + unlikely(!vq->vring.avail)) { + return true; + } + + if (vq->shadow_avail_idx !=3D vq->last_avail_idx) { + return false; + } + + return vring_avail_idx(vq) =3D=3D vq->last_avail_idx; +} + +static bool +vring_notify(VuDev *dev, VuVirtq *vq) +{ + uint16_t old, new; + bool v; + + /* We need to expose used array entries before checking used event. */ + smp_mb(); + + /* Always notify when queue is empty (when feature acknowledge) */ + if (vu_has_feature(dev, VIRTIO_F_NOTIFY_ON_EMPTY) && + !vq->inuse && vu_queue_empty(dev, vq)) { + return true; + } + + if (!vu_has_feature(dev, VIRTIO_RING_F_EVENT_IDX)) { + return !(vring_avail_flags(vq) & VRING_AVAIL_F_NO_INTERRUPT); + } + + v =3D vq->signalled_used_valid; + vq->signalled_used_valid =3D true; + old =3D vq->signalled_used; + new =3D vq->signalled_used =3D vq->used_idx; + return !v || vring_need_event(vring_get_used_event(vq), new, old); +} + +static void _vu_queue_notify(VuDev *dev, VuVirtq *vq, bool sync) +{ + if (unlikely(dev->broken) || + unlikely(!vq->vring.avail)) { + return; + } + + if (!vring_notify(dev, vq)) { + DPRINT("skipped notify...\n"); + return; + } + + if (vq->call_fd < 0 && + vu_has_protocol_feature(dev, + VHOST_USER_PROTOCOL_F_INBAND_NOTIFICATIONS= ) && + vu_has_protocol_feature(dev, VHOST_USER_PROTOCOL_F_SLAVE_REQ)) { + VhostUserMsg vmsg =3D { + .request =3D VHOST_USER_SLAVE_VRING_CALL, + .flags =3D VHOST_USER_VERSION, + .size =3D sizeof(vmsg.payload.state), + .payload.state =3D { + .index =3D vq - dev->vq, + }, + }; + bool ack =3D sync && + vu_has_protocol_feature(dev, + VHOST_USER_PROTOCOL_F_REPLY_ACK= ); + + if (ack) { + vmsg.flags |=3D VHOST_USER_NEED_REPLY_MASK; + } + + vu_message_write(dev, dev->slave_fd, &vmsg); + if (ack) { + vu_message_read_default(dev, dev->slave_fd, &vmsg); + } + return; + } + + if (eventfd_write(vq->call_fd, 1) < 0) { + vu_panic(dev, "Error writing eventfd: %s", strerror(errno)); + } +} + +void vu_queue_notify(VuDev *dev, VuVirtq *vq) +{ + _vu_queue_notify(dev, vq, false); +} + +void vu_queue_notify_sync(VuDev *dev, VuVirtq *vq) +{ + _vu_queue_notify(dev, vq, true); +} + +static inline void +vring_used_flags_set_bit(VuVirtq *vq, int mask) +{ + uint16_t *flags; + + flags =3D (uint16_t *)((char*)vq->vring.used + + offsetof(struct vring_used, flags)); + *flags =3D htole16(le16toh(*flags) | mask); +} + +static inline void +vring_used_flags_unset_bit(VuVirtq *vq, int mask) +{ + uint16_t *flags; + + flags =3D (uint16_t *)((char*)vq->vring.used + + offsetof(struct vring_used, flags)); + *flags =3D htole16(le16toh(*flags) & ~mask); +} + +static inline void +vring_set_avail_event(VuVirtq *vq, uint16_t val) +{ + uint16_t *avail; + + if (!vq->notification) { + return; + } + + avail =3D (uint16_t *)&vq->vring.used->ring[vq->vring.num]; + *avail =3D htole16(val); +} + +void +vu_queue_set_notification(VuDev *dev, VuVirtq *vq, int enable) +{ + vq->notification =3D enable; + if (vu_has_feature(dev, VIRTIO_RING_F_EVENT_IDX)) { + vring_set_avail_event(vq, vring_avail_idx(vq)); + } else if (enable) { + vring_used_flags_unset_bit(vq, VRING_USED_F_NO_NOTIFY); + } else { + vring_used_flags_set_bit(vq, VRING_USED_F_NO_NOTIFY); + } + if (enable) { + /* Expose avail event/used flags before caller checks the avail id= x. */ + smp_mb(); + } +} + +static bool +virtqueue_map_desc(VuDev *dev, + unsigned int *p_num_sg, struct iovec *iov, + unsigned int max_num_sg, bool is_write, + uint64_t pa, size_t sz) +{ + unsigned num_sg =3D *p_num_sg; + + assert(num_sg <=3D max_num_sg); + + if (!sz) { + vu_panic(dev, "virtio: zero sized buffers are not allowed"); + return false; + } + + while (sz) { + uint64_t len =3D sz; + + if (num_sg =3D=3D max_num_sg) { + vu_panic(dev, "virtio: too many descriptors in indirect table"= ); + return false; + } + + iov[num_sg].iov_base =3D vu_gpa_to_va(dev, &len, pa); + if (iov[num_sg].iov_base =3D=3D NULL) { + vu_panic(dev, "virtio: invalid address for buffers"); + return false; + } + iov[num_sg].iov_len =3D len; + num_sg++; + sz -=3D len; + pa +=3D len; + } + + *p_num_sg =3D num_sg; + return true; +} + +static void * +virtqueue_alloc_element(size_t sz, + unsigned out_num, unsigned in_num) +{ + VuVirtqElement *elem; + size_t in_sg_ofs =3D ALIGN_UP(sz, __alignof__(elem->in_sg[0])); + size_t out_sg_ofs =3D in_sg_ofs + in_num * sizeof(elem->in_sg[0]); + size_t out_sg_end =3D out_sg_ofs + out_num * sizeof(elem->out_sg[0]); + + assert(sz >=3D sizeof(VuVirtqElement)); + elem =3D malloc(out_sg_end); + elem->out_num =3D out_num; + elem->in_num =3D in_num; + elem->in_sg =3D (void *)elem + in_sg_ofs; + elem->out_sg =3D (void *)elem + out_sg_ofs; + return elem; +} + +static void * +vu_queue_map_desc(VuDev *dev, VuVirtq *vq, unsigned int idx, size_t sz) +{ + struct vring_desc *desc =3D vq->vring.desc; + uint64_t desc_addr, read_len; + unsigned int desc_len; + unsigned int max =3D vq->vring.num; + unsigned int i =3D idx; + VuVirtqElement *elem; + unsigned int out_num =3D 0, in_num =3D 0; + struct iovec iov[VIRTQUEUE_MAX_SIZE]; + struct vring_desc desc_buf[VIRTQUEUE_MAX_SIZE]; + int rc; + + if (le16toh(desc[i].flags) & VRING_DESC_F_INDIRECT) { + if (le32toh(desc[i].len) % sizeof(struct vring_desc)) { + vu_panic(dev, "Invalid size for indirect buffer table"); + return NULL; + } + + /* loop over the indirect descriptor table */ + desc_addr =3D le64toh(desc[i].addr); + desc_len =3D le32toh(desc[i].len); + max =3D desc_len / sizeof(struct vring_desc); + read_len =3D desc_len; + desc =3D vu_gpa_to_va(dev, &read_len, desc_addr); + if (unlikely(desc && read_len !=3D desc_len)) { + /* Failed to use zero copy */ + desc =3D NULL; + if (!virtqueue_read_indirect_desc(dev, desc_buf, + desc_addr, + desc_len)) { + desc =3D desc_buf; + } + } + if (!desc) { + vu_panic(dev, "Invalid indirect buffer table"); + return NULL; + } + i =3D 0; + } + + /* Collect all the descriptors */ + do { + if (le16toh(desc[i].flags) & VRING_DESC_F_WRITE) { + if (!virtqueue_map_desc(dev, &in_num, iov + out_num, + VIRTQUEUE_MAX_SIZE - out_num, true, + le64toh(desc[i].addr), + le32toh(desc[i].len))) { + return NULL; + } + } else { + if (in_num) { + vu_panic(dev, "Incorrect order for descriptors"); + return NULL; + } + if (!virtqueue_map_desc(dev, &out_num, iov, + VIRTQUEUE_MAX_SIZE, false, + le64toh(desc[i].addr), + le32toh(desc[i].len))) { + return NULL; + } + } + + /* If we've got too many, that implies a descriptor loop. */ + if ((in_num + out_num) > max) { + vu_panic(dev, "Looped descriptor"); + return NULL; + } + rc =3D virtqueue_read_next_desc(dev, desc, i, max, &i); + } while (rc =3D=3D VIRTQUEUE_READ_DESC_MORE); + + if (rc =3D=3D VIRTQUEUE_READ_DESC_ERROR) { + vu_panic(dev, "read descriptor error"); + return NULL; + } + + /* Now copy what we have collected and mapped */ + elem =3D virtqueue_alloc_element(sz, out_num, in_num); + elem->index =3D idx; + for (i =3D 0; i < out_num; i++) { + elem->out_sg[i] =3D iov[i]; + } + for (i =3D 0; i < in_num; i++) { + elem->in_sg[i] =3D iov[out_num + i]; + } + + return elem; +} + +static int +vu_queue_inflight_get(VuDev *dev, VuVirtq *vq, int desc_idx) +{ + if (!vu_has_protocol_feature(dev, VHOST_USER_PROTOCOL_F_INFLIGHT_SHMFD= )) { + return 0; + } + + if (unlikely(!vq->inflight)) { + return -1; + } + + vq->inflight->desc[desc_idx].counter =3D vq->counter++; + vq->inflight->desc[desc_idx].inflight =3D 1; + + return 0; +} + +static int +vu_queue_inflight_pre_put(VuDev *dev, VuVirtq *vq, int desc_idx) +{ + if (!vu_has_protocol_feature(dev, VHOST_USER_PROTOCOL_F_INFLIGHT_SHMFD= )) { + return 0; + } + + if (unlikely(!vq->inflight)) { + return -1; + } + + vq->inflight->last_batch_head =3D desc_idx; + + return 0; +} + +static int +vu_queue_inflight_post_put(VuDev *dev, VuVirtq *vq, int desc_idx) +{ + if (!vu_has_protocol_feature(dev, VHOST_USER_PROTOCOL_F_INFLIGHT_SHMFD= )) { + return 0; + } + + if (unlikely(!vq->inflight)) { + return -1; + } + + barrier(); + + vq->inflight->desc[desc_idx].inflight =3D 0; + + barrier(); + + vq->inflight->used_idx =3D vq->used_idx; + + return 0; +} + +void * +vu_queue_pop(VuDev *dev, VuVirtq *vq, size_t sz) +{ + int i; + unsigned int head; + VuVirtqElement *elem; + + if (unlikely(dev->broken) || + unlikely(!vq->vring.avail)) { + return NULL; + } + + if (unlikely(vq->resubmit_list && vq->resubmit_num > 0)) { + i =3D (--vq->resubmit_num); + elem =3D vu_queue_map_desc(dev, vq, vq->resubmit_list[i].index, sz= ); + + if (!vq->resubmit_num) { + free(vq->resubmit_list); + vq->resubmit_list =3D NULL; + } + + return elem; + } + + if (vu_queue_empty(dev, vq)) { + return NULL; + } + /* + * Needed after virtio_queue_empty(), see comment in + * virtqueue_num_heads(). + */ + smp_rmb(); + + if (vq->inuse >=3D vq->vring.num) { + vu_panic(dev, "Virtqueue size exceeded"); + return NULL; + } + + if (!virtqueue_get_head(dev, vq, vq->last_avail_idx++, &head)) { + return NULL; + } + + if (vu_has_feature(dev, VIRTIO_RING_F_EVENT_IDX)) { + vring_set_avail_event(vq, vq->last_avail_idx); + } + + elem =3D vu_queue_map_desc(dev, vq, head, sz); + + if (!elem) { + return NULL; + } + + vq->inuse++; + + vu_queue_inflight_get(dev, vq, head); + + return elem; +} + +static void +vu_queue_detach_element(VuDev *dev, VuVirtq *vq, VuVirtqElement *elem, + size_t len) +{ + vq->inuse--; + /* unmap, when DMA support is added */ +} + +void +vu_queue_unpop(VuDev *dev, VuVirtq *vq, VuVirtqElement *elem, + size_t len) +{ + vq->last_avail_idx--; + vu_queue_detach_element(dev, vq, elem, len); +} + +bool +vu_queue_rewind(VuDev *dev, VuVirtq *vq, unsigned int num) +{ + if (num > vq->inuse) { + return false; + } + vq->last_avail_idx -=3D num; + vq->inuse -=3D num; + return true; +} + +static inline +void vring_used_write(VuDev *dev, VuVirtq *vq, + struct vring_used_elem *uelem, int i) +{ + struct vring_used *used =3D vq->vring.used; + + used->ring[i] =3D *uelem; + vu_log_write(dev, vq->vring.log_guest_addr + + offsetof(struct vring_used, ring[i]), + sizeof(used->ring[i])); +} + + +static void +vu_log_queue_fill(VuDev *dev, VuVirtq *vq, + const VuVirtqElement *elem, + unsigned int len) +{ + struct vring_desc *desc =3D vq->vring.desc; + unsigned int i, max, min, desc_len; + uint64_t desc_addr, read_len; + struct vring_desc desc_buf[VIRTQUEUE_MAX_SIZE]; + unsigned num_bufs =3D 0; + + max =3D vq->vring.num; + i =3D elem->index; + + if (le16toh(desc[i].flags) & VRING_DESC_F_INDIRECT) { + if (le32toh(desc[i].len) % sizeof(struct vring_desc)) { + vu_panic(dev, "Invalid size for indirect buffer table"); + return; + } + + /* loop over the indirect descriptor table */ + desc_addr =3D le64toh(desc[i].addr); + desc_len =3D le32toh(desc[i].len); + max =3D desc_len / sizeof(struct vring_desc); + read_len =3D desc_len; + desc =3D vu_gpa_to_va(dev, &read_len, desc_addr); + if (unlikely(desc && read_len !=3D desc_len)) { + /* Failed to use zero copy */ + desc =3D NULL; + if (!virtqueue_read_indirect_desc(dev, desc_buf, + desc_addr, + desc_len)) { + desc =3D desc_buf; + } + } + if (!desc) { + vu_panic(dev, "Invalid indirect buffer table"); + return; + } + i =3D 0; + } + + do { + if (++num_bufs > max) { + vu_panic(dev, "Looped descriptor"); + return; + } + + if (le16toh(desc[i].flags) & VRING_DESC_F_WRITE) { + min =3D MIN(le32toh(desc[i].len), len); + vu_log_write(dev, le64toh(desc[i].addr), min); + len -=3D min; + } + + } while (len > 0 && + (virtqueue_read_next_desc(dev, desc, i, max, &i) + =3D=3D VIRTQUEUE_READ_DESC_MORE)); +} + +void +vu_queue_fill(VuDev *dev, VuVirtq *vq, + const VuVirtqElement *elem, + unsigned int len, unsigned int idx) +{ + struct vring_used_elem uelem; + + if (unlikely(dev->broken) || + unlikely(!vq->vring.avail)) { + return; + } + + vu_log_queue_fill(dev, vq, elem, len); + + idx =3D (idx + vq->used_idx) % vq->vring.num; + + uelem.id =3D htole32(elem->index); + uelem.len =3D htole32(len); + vring_used_write(dev, vq, &uelem, idx); +} + +static inline +void vring_used_idx_set(VuDev *dev, VuVirtq *vq, uint16_t val) +{ + vq->vring.used->idx =3D htole16(val); + vu_log_write(dev, + vq->vring.log_guest_addr + offsetof(struct vring_used, id= x), + sizeof(vq->vring.used->idx)); + + vq->used_idx =3D val; +} + +void +vu_queue_flush(VuDev *dev, VuVirtq *vq, unsigned int count) +{ + uint16_t old, new; + + if (unlikely(dev->broken) || + unlikely(!vq->vring.avail)) { + return; + } + + /* Make sure buffer is written before we update index. */ + smp_wmb(); + + old =3D vq->used_idx; + new =3D old + count; + vring_used_idx_set(dev, vq, new); + vq->inuse -=3D count; + if (unlikely((int16_t)(new - vq->signalled_used) < (uint16_t)(new - ol= d))) { + vq->signalled_used_valid =3D false; + } +} + +void +vu_queue_push(VuDev *dev, VuVirtq *vq, + const VuVirtqElement *elem, unsigned int len) +{ + vu_queue_fill(dev, vq, elem, len, 0); + vu_queue_inflight_pre_put(dev, vq, elem->index); + vu_queue_flush(dev, vq, 1); + vu_queue_inflight_post_put(dev, vq, elem->index); +} diff --git a/tools/testing/roadtest/src/libvhost-user/libvhost-user.h b/too= ls/testing/roadtest/src/libvhost-user/libvhost-user.h new file mode 100644 index 000000000000..691e0bc051c1 --- /dev/null +++ b/tools/testing/roadtest/src/libvhost-user/libvhost-user.h @@ -0,0 +1,691 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Vhost User library + * + * Copyright (c) 2016 Red Hat, Inc. + * + * Authors: + * Victor Kaplansky + * Marc-Andr=C3=A9 Lureau + * + * This work is licensed under the terms of the GNU GPL, version 2 or + * later. See the COPYING file in the top-level directory. + */ + +#ifndef LIBVHOST_USER_H +#define LIBVHOST_USER_H + +#include +#include +#include +#include +#include +#include +#include + +/* Based on qemu/hw/virtio/vhost-user.c */ +#define VHOST_USER_F_PROTOCOL_FEATURES 30 +#define VHOST_LOG_PAGE 4096 + +#define VIRTQUEUE_MAX_SIZE 1024 + +#define VHOST_MEMORY_BASELINE_NREGIONS 8 + +/* + * Set a reasonable maximum number of ram slots, which will be supported by + * any architecture. + */ +#define VHOST_USER_MAX_RAM_SLOTS 32 + +#define VHOST_USER_HDR_SIZE offsetof(VhostUserMsg, payload.u64) + +typedef enum VhostSetConfigType { + VHOST_SET_CONFIG_TYPE_MASTER =3D 0, + VHOST_SET_CONFIG_TYPE_MIGRATION =3D 1, +} VhostSetConfigType; + +/* + * Maximum size of virtio device config space + */ +#define VHOST_USER_MAX_CONFIG_SIZE 256 + +enum VhostUserProtocolFeature { + VHOST_USER_PROTOCOL_F_MQ =3D 0, + VHOST_USER_PROTOCOL_F_LOG_SHMFD =3D 1, + VHOST_USER_PROTOCOL_F_RARP =3D 2, + VHOST_USER_PROTOCOL_F_REPLY_ACK =3D 3, + VHOST_USER_PROTOCOL_F_NET_MTU =3D 4, + VHOST_USER_PROTOCOL_F_SLAVE_REQ =3D 5, + VHOST_USER_PROTOCOL_F_CROSS_ENDIAN =3D 6, + VHOST_USER_PROTOCOL_F_CRYPTO_SESSION =3D 7, + VHOST_USER_PROTOCOL_F_PAGEFAULT =3D 8, + VHOST_USER_PROTOCOL_F_CONFIG =3D 9, + VHOST_USER_PROTOCOL_F_SLAVE_SEND_FD =3D 10, + VHOST_USER_PROTOCOL_F_HOST_NOTIFIER =3D 11, + VHOST_USER_PROTOCOL_F_INFLIGHT_SHMFD =3D 12, + VHOST_USER_PROTOCOL_F_INBAND_NOTIFICATIONS =3D 14, + VHOST_USER_PROTOCOL_F_CONFIGURE_MEM_SLOTS =3D 15, + + VHOST_USER_PROTOCOL_F_MAX +}; + +#define VHOST_USER_PROTOCOL_FEATURE_MASK ((1 << VHOST_USER_PROTOCOL_F_MAX)= - 1) + +typedef enum VhostUserRequest { + VHOST_USER_NONE =3D 0, + VHOST_USER_GET_FEATURES =3D 1, + VHOST_USER_SET_FEATURES =3D 2, + VHOST_USER_SET_OWNER =3D 3, + VHOST_USER_RESET_OWNER =3D 4, + VHOST_USER_SET_MEM_TABLE =3D 5, + VHOST_USER_SET_LOG_BASE =3D 6, + VHOST_USER_SET_LOG_FD =3D 7, + VHOST_USER_SET_VRING_NUM =3D 8, + VHOST_USER_SET_VRING_ADDR =3D 9, + VHOST_USER_SET_VRING_BASE =3D 10, + VHOST_USER_GET_VRING_BASE =3D 11, + VHOST_USER_SET_VRING_KICK =3D 12, + VHOST_USER_SET_VRING_CALL =3D 13, + VHOST_USER_SET_VRING_ERR =3D 14, + VHOST_USER_GET_PROTOCOL_FEATURES =3D 15, + VHOST_USER_SET_PROTOCOL_FEATURES =3D 16, + VHOST_USER_GET_QUEUE_NUM =3D 17, + VHOST_USER_SET_VRING_ENABLE =3D 18, + VHOST_USER_SEND_RARP =3D 19, + VHOST_USER_NET_SET_MTU =3D 20, + VHOST_USER_SET_SLAVE_REQ_FD =3D 21, + VHOST_USER_IOTLB_MSG =3D 22, + VHOST_USER_SET_VRING_ENDIAN =3D 23, + VHOST_USER_GET_CONFIG =3D 24, + VHOST_USER_SET_CONFIG =3D 25, + VHOST_USER_CREATE_CRYPTO_SESSION =3D 26, + VHOST_USER_CLOSE_CRYPTO_SESSION =3D 27, + VHOST_USER_POSTCOPY_ADVISE =3D 28, + VHOST_USER_POSTCOPY_LISTEN =3D 29, + VHOST_USER_POSTCOPY_END =3D 30, + VHOST_USER_GET_INFLIGHT_FD =3D 31, + VHOST_USER_SET_INFLIGHT_FD =3D 32, + VHOST_USER_GPU_SET_SOCKET =3D 33, + VHOST_USER_VRING_KICK =3D 35, + VHOST_USER_GET_MAX_MEM_SLOTS =3D 36, + VHOST_USER_ADD_MEM_REG =3D 37, + VHOST_USER_REM_MEM_REG =3D 38, + VHOST_USER_MAX +} VhostUserRequest; + +typedef enum VhostUserSlaveRequest { + VHOST_USER_SLAVE_NONE =3D 0, + VHOST_USER_SLAVE_IOTLB_MSG =3D 1, + VHOST_USER_SLAVE_CONFIG_CHANGE_MSG =3D 2, + VHOST_USER_SLAVE_VRING_HOST_NOTIFIER_MSG =3D 3, + VHOST_USER_SLAVE_VRING_CALL =3D 4, + VHOST_USER_SLAVE_VRING_ERR =3D 5, + VHOST_USER_SLAVE_MAX +} VhostUserSlaveRequest; + +typedef struct VhostUserMemoryRegion { + uint64_t guest_phys_addr; + uint64_t memory_size; + uint64_t userspace_addr; + uint64_t mmap_offset; +} VhostUserMemoryRegion; + +typedef struct VhostUserMemory { + uint32_t nregions; + uint32_t padding; + VhostUserMemoryRegion regions[VHOST_MEMORY_BASELINE_NREGIONS]; +} VhostUserMemory; + +typedef struct VhostUserMemRegMsg { + uint64_t padding; + VhostUserMemoryRegion region; +} VhostUserMemRegMsg; + +typedef struct VhostUserLog { + uint64_t mmap_size; + uint64_t mmap_offset; +} VhostUserLog; + +typedef struct VhostUserConfig { + uint32_t offset; + uint32_t size; + uint32_t flags; + uint8_t region[VHOST_USER_MAX_CONFIG_SIZE]; +} VhostUserConfig; + +static VhostUserConfig c __attribute__ ((unused)); +#define VHOST_USER_CONFIG_HDR_SIZE (sizeof(c.offset) \ + + sizeof(c.size) \ + + sizeof(c.flags)) + +typedef struct VhostUserVringArea { + uint64_t u64; + uint64_t size; + uint64_t offset; +} VhostUserVringArea; + +typedef struct VhostUserInflight { + uint64_t mmap_size; + uint64_t mmap_offset; + uint16_t num_queues; + uint16_t queue_size; +} VhostUserInflight; + +#if defined(_WIN32) && (defined(__x86_64__) || defined(__i386__)) +# define VU_PACKED __attribute__((gcc_struct, packed)) +#else +# define VU_PACKED __attribute__((packed)) +#endif + +typedef struct VhostUserMsg { + int request; + +#define VHOST_USER_VERSION_MASK (0x3) +#define VHOST_USER_REPLY_MASK (0x1 << 2) +#define VHOST_USER_NEED_REPLY_MASK (0x1 << 3) + uint32_t flags; + uint32_t size; /* the following payload size */ + + union { +#define VHOST_USER_VRING_IDX_MASK (0xff) +#define VHOST_USER_VRING_NOFD_MASK (0x1 << 8) + uint64_t u64; + struct vhost_vring_state state; + struct vhost_vring_addr addr; + VhostUserMemory memory; + VhostUserMemRegMsg memreg; + VhostUserLog log; + VhostUserConfig config; + VhostUserVringArea area; + VhostUserInflight inflight; + } payload; + + int fds[VHOST_MEMORY_BASELINE_NREGIONS]; + int fd_num; + uint8_t *data; +} VU_PACKED VhostUserMsg; + +typedef struct VuDevRegion { + /* Guest Physical address. */ + uint64_t gpa; + /* Memory region size. */ + uint64_t size; + /* QEMU virtual address (userspace). */ + uint64_t qva; + /* Starting offset in our mmaped space. */ + uint64_t mmap_offset; + /* Start address of mmaped space. */ + uint64_t mmap_addr; +} VuDevRegion; + +typedef struct VuDev VuDev; + +typedef uint64_t (*vu_get_features_cb) (VuDev *dev); +typedef void (*vu_set_features_cb) (VuDev *dev, uint64_t features); +typedef int (*vu_process_msg_cb) (VuDev *dev, VhostUserMsg *vmsg, + int *do_reply); +typedef bool (*vu_read_msg_cb) (VuDev *dev, int sock, VhostUserMsg *vmsg); +typedef void (*vu_queue_set_started_cb) (VuDev *dev, int qidx, bool starte= d); +typedef bool (*vu_queue_is_processed_in_order_cb) (VuDev *dev, int qidx); +typedef int (*vu_get_config_cb) (VuDev *dev, uint8_t *config, uint32_t len= ); +typedef int (*vu_set_config_cb) (VuDev *dev, const uint8_t *data, + uint32_t offset, uint32_t size, + uint32_t flags); + +typedef struct VuDevIface { + /* called by VHOST_USER_GET_FEATURES to get the features bitmask */ + vu_get_features_cb get_features; + /* enable vhost implementation features */ + vu_set_features_cb set_features; + /* get the protocol feature bitmask from the underlying vhost + * implementation */ + vu_get_features_cb get_protocol_features; + /* enable protocol features in the underlying vhost implementation. */ + vu_set_features_cb set_protocol_features; + /* process_msg is called for each vhost-user message received */ + /* skip libvhost-user processing if return value !=3D 0 */ + vu_process_msg_cb process_msg; + /* tells when queues can be processed */ + vu_queue_set_started_cb queue_set_started; + /* + * If the queue is processed in order, in which case it will be + * resumed to vring.used->idx. This can help to support resuming + * on unmanaged exit/crash. + */ + vu_queue_is_processed_in_order_cb queue_is_processed_in_order; + /* get the config space of the device */ + vu_get_config_cb get_config; + /* set the config space of the device */ + vu_set_config_cb set_config; +} VuDevIface; + +typedef void (*vu_queue_handler_cb) (VuDev *dev, int qidx); + +typedef struct VuRing { + unsigned int num; + struct vring_desc *desc; + struct vring_avail *avail; + struct vring_used *used; + uint64_t log_guest_addr; + uint32_t flags; +} VuRing; + +typedef struct VuDescStateSplit { + /* Indicate whether this descriptor is inflight or not. + * Only available for head-descriptor. */ + uint8_t inflight; + + /* Padding */ + uint8_t padding[5]; + + /* Maintain a list for the last batch of used descriptors. + * Only available when batching is used for submitting */ + uint16_t next; + + /* Used to preserve the order of fetching available descriptors. + * Only available for head-descriptor. */ + uint64_t counter; +} VuDescStateSplit; + +typedef struct VuVirtqInflight { + /* The feature flags of this region. Now it's initialized to 0. */ + uint64_t features; + + /* The version of this region. It's 1 currently. + * Zero value indicates a vm reset happened. */ + uint16_t version; + + /* The size of VuDescStateSplit array. It's equal to the virtqueue + * size. Slave could get it from queue size field of VhostUserInflight= . */ + uint16_t desc_num; + + /* The head of list that track the last batch of used descriptors. */ + uint16_t last_batch_head; + + /* Storing the idx value of used ring */ + uint16_t used_idx; + + /* Used to track the state of each descriptor in descriptor table */ + VuDescStateSplit desc[]; +} VuVirtqInflight; + +typedef struct VuVirtqInflightDesc { + uint16_t index; + uint64_t counter; +} VuVirtqInflightDesc; + +typedef struct VuVirtq { + VuRing vring; + + VuVirtqInflight *inflight; + + VuVirtqInflightDesc *resubmit_list; + + uint16_t resubmit_num; + + uint64_t counter; + + /* Next head to pop */ + uint16_t last_avail_idx; + + /* Last avail_idx read from VQ. */ + uint16_t shadow_avail_idx; + + uint16_t used_idx; + + /* Last used index value we have signalled on */ + uint16_t signalled_used; + + /* Last used index value we have signalled on */ + bool signalled_used_valid; + + /* Notification enabled? */ + bool notification; + + int inuse; + + vu_queue_handler_cb handler; + + int call_fd; + int kick_fd; + int err_fd; + unsigned int enable; + bool started; + + /* Guest addresses of our ring */ + struct vhost_vring_addr vra; +} VuVirtq; + +enum VuWatchCondtion { + VU_WATCH_IN =3D POLLIN, + VU_WATCH_OUT =3D POLLOUT, + VU_WATCH_PRI =3D POLLPRI, + VU_WATCH_ERR =3D POLLERR, + VU_WATCH_HUP =3D POLLHUP, +}; + +typedef void (*vu_panic_cb) (VuDev *dev, const char *err); +typedef void (*vu_watch_cb) (VuDev *dev, int condition, void *data); +typedef void (*vu_set_watch_cb) (VuDev *dev, int fd, int condition, + vu_watch_cb cb, void *data); +typedef void (*vu_remove_watch_cb) (VuDev *dev, int fd); + +typedef struct VuDevInflightInfo { + int fd; + void *addr; + uint64_t size; +} VuDevInflightInfo; + +struct VuDev { + int sock; + uint32_t nregions; + VuDevRegion regions[VHOST_USER_MAX_RAM_SLOTS]; + VuVirtq *vq; + VuDevInflightInfo inflight_info; + int log_call_fd; + /* Must be held while using slave_fd */ + pthread_mutex_t slave_mutex; + int slave_fd; + uint64_t log_size; + uint8_t *log_table; + uint64_t features; + uint64_t protocol_features; + bool broken; + uint16_t max_queues; + + /* + * @read_msg: custom method to read vhost-user message + * + * Read data from vhost_user socket fd and fill up + * the passed VhostUserMsg *vmsg struct. + * + * If reading fails, it should close the received set of file + * descriptors as socket message's auxiliary data. + * + * For the details, please refer to vu_message_read in libvhost-user.c + * which will be used by default if not custom method is provided when + * calling vu_init + * + * Returns: true if vhost-user message successfully received, + * otherwise return false. + * + */ + vu_read_msg_cb read_msg; + + /* + * @set_watch: add or update the given fd to the watch set, + * call cb when condition is met. + */ + vu_set_watch_cb set_watch; + + /* @remove_watch: remove the given fd from the watch set */ + vu_remove_watch_cb remove_watch; + + /* + * @panic: encountered an unrecoverable error, you may try to re-initi= alize + */ + vu_panic_cb panic; + const VuDevIface *iface; + + /* Postcopy data */ + int postcopy_ufd; + bool postcopy_listening; +}; + +typedef struct VuVirtqElement { + unsigned int index; + unsigned int out_num; + unsigned int in_num; + struct iovec *in_sg; + struct iovec *out_sg; +} VuVirtqElement; + +/** + * vu_init: + * @dev: a VuDev context + * @max_queues: maximum number of virtqueues + * @socket: the socket connected to vhost-user master + * @panic: a panic callback + * @set_watch: a set_watch callback + * @remove_watch: a remove_watch callback + * @iface: a VuDevIface structure with vhost-user device callbacks + * + * Initializes a VuDev vhost-user context. + * + * Returns: true on success, false on failure. + **/ +bool vu_init(VuDev *dev, + uint16_t max_queues, + int socket, + vu_panic_cb panic, + vu_read_msg_cb read_msg, + vu_set_watch_cb set_watch, + vu_remove_watch_cb remove_watch, + const VuDevIface *iface); + + +/** + * vu_deinit: + * @dev: a VuDev context + * + * Cleans up the VuDev context + */ +void vu_deinit(VuDev *dev); + +/** + * vu_dispatch: + * @dev: a VuDev context + * + * Process one vhost-user message. + * + * Returns: TRUE on success, FALSE on failure. + */ +bool vu_dispatch(VuDev *dev); + +/** + * vu_gpa_to_va: + * @dev: a VuDev context + * @plen: guest memory size + * @guest_addr: guest address + * + * Translate a guest address to a pointer. Returns NULL on failure. + */ +void *vu_gpa_to_va(VuDev *dev, uint64_t *plen, uint64_t guest_addr); + +/** + * vu_get_queue: + * @dev: a VuDev context + * @qidx: queue index + * + * Returns the queue number @qidx. + */ +VuVirtq *vu_get_queue(VuDev *dev, int qidx); + +/** + * vu_set_queue_handler: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * @handler: the queue handler callback + * + * Set the queue handler. This function may be called several times + * for the same queue. If called with NULL @handler, the handler is + * removed. + */ +void vu_set_queue_handler(VuDev *dev, VuVirtq *vq, + vu_queue_handler_cb handler); + +/** + * vu_set_queue_host_notifier: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * @fd: a file descriptor + * @size: host page size + * @offset: notifier offset in @fd file + * + * Set queue's host notifier. This function may be called several + * times for the same queue. If called with -1 @fd, the notifier + * is removed. + */ +bool vu_set_queue_host_notifier(VuDev *dev, VuVirtq *vq, int fd, + int size, int offset); + +/** + * vu_queue_set_notification: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * @enable: state + * + * Set whether the queue notifies (via event index or interrupt) + */ +void vu_queue_set_notification(VuDev *dev, VuVirtq *vq, int enable); + +/** + * vu_queue_enabled: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * + * Returns: whether the queue is enabled. + */ +bool vu_queue_enabled(VuDev *dev, VuVirtq *vq); + +/** + * vu_queue_started: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * + * Returns: whether the queue is started. + */ +bool vu_queue_started(const VuDev *dev, const VuVirtq *vq); + +/** + * vu_queue_empty: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * + * Returns: true if the queue is empty or not ready. + */ +bool vu_queue_empty(VuDev *dev, VuVirtq *vq); + +/** + * vu_queue_notify: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * + * Request to notify the queue via callfd (skipped if unnecessary) + */ +void vu_queue_notify(VuDev *dev, VuVirtq *vq); + +/** + * vu_queue_notify_sync: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * + * Request to notify the queue via callfd (skipped if unnecessary) + * or sync message if possible. + */ +void vu_queue_notify_sync(VuDev *dev, VuVirtq *vq); + +/** + * vu_queue_pop: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * @sz: the size of struct to return (must be >=3D VuVirtqElement) + * + * Returns: a VuVirtqElement filled from the queue or NULL. The + * returned element must be free()-d by the caller. + */ +void *vu_queue_pop(VuDev *dev, VuVirtq *vq, size_t sz); + + +/** + * vu_queue_unpop: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * @elem: The #VuVirtqElement + * @len: number of bytes written + * + * Pretend the most recent element wasn't popped from the virtqueue. The = next + * call to vu_queue_pop() will refetch the element. + */ +void vu_queue_unpop(VuDev *dev, VuVirtq *vq, VuVirtqElement *elem, + size_t len); + +/** + * vu_queue_rewind: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * @num: number of elements to push back + * + * Pretend that elements weren't popped from the virtqueue. The next + * virtqueue_pop() will refetch the oldest element. + * + * Returns: true on success, false if @num is greater than the number of i= n use + * elements. + */ +bool vu_queue_rewind(VuDev *dev, VuVirtq *vq, unsigned int num); + +/** + * vu_queue_fill: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * @elem: a VuVirtqElement + * @len: length in bytes to write + * @idx: optional offset for the used ring index (0 in general) + * + * Fill the used ring with @elem element. + */ +void vu_queue_fill(VuDev *dev, VuVirtq *vq, + const VuVirtqElement *elem, + unsigned int len, unsigned int idx); + +/** + * vu_queue_push: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * @elem: a VuVirtqElement + * @len: length in bytes to write + * + * Helper that combines vu_queue_fill() with a vu_queue_flush(). + */ +void vu_queue_push(VuDev *dev, VuVirtq *vq, + const VuVirtqElement *elem, unsigned int len); + +/** + * vu_queue_flush: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * @num: number of elements to flush + * + * Mark the last number of elements as done (used.idx is updated by + * num elements). +*/ +void vu_queue_flush(VuDev *dev, VuVirtq *vq, unsigned int num); + +/** + * vu_queue_get_avail_bytes: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * @in_bytes: in bytes + * @out_bytes: out bytes + * @max_in_bytes: stop counting after max_in_bytes + * @max_out_bytes: stop counting after max_out_bytes + * + * Count the number of available bytes, up to max_in_bytes/max_out_bytes. + */ +void vu_queue_get_avail_bytes(VuDev *vdev, VuVirtq *vq, unsigned int *in_b= ytes, + unsigned int *out_bytes, + unsigned max_in_bytes, unsigned max_out_byte= s); + +/** + * vu_queue_avail_bytes: + * @dev: a VuDev context + * @vq: a VuVirtq queue + * @in_bytes: expected in bytes + * @out_bytes: expected out bytes + * + * Returns: true if in_bytes <=3D in_total && out_bytes <=3D out_total + */ +bool vu_queue_avail_bytes(VuDev *dev, VuVirtq *vq, unsigned int in_bytes, + unsigned int out_bytes); + +#endif /* LIBVHOST_USER_H */ --=20 2.34.1 From nobody Tue Jun 23 03:06:50 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id CAE98C433F5 for ; Fri, 11 Mar 2022 16:25:38 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1350314AbiCKQ0k (ORCPT ); Fri, 11 Mar 2022 11:26:40 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:37626 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350313AbiCKQ0d (ORCPT ); Fri, 11 Mar 2022 11:26:33 -0500 Received: from smtp1.axis.com (smtp1.axis.com [195.60.68.17]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 61940117C9E; Fri, 11 Mar 2022 08:24:50 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015895; x=1678551895; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=rpTqoknbDOn72Q+UH1BdHCLF3yJZId72jHd8FypIw5U=; b=KcTKg2XaTrn9jOPkhoLZo2g8hqlxgAXxCaFNkEUNe/XZTLF/5fsg9L3y OdLoqHasFsEuGvrL3LfvL090oX71yeOWW3ecNOCg5CRf+I4/teuL4zz8q zMRpnGKbSJm78pKxDDIf8lPxKmJ7h5A7ZoY7fCl1J8428NVg2XHRtx3IP HamRXuPpMPfjpfU5el/1ezL+8usJUFiYcUOUkQq50sZPpsE5RhVzmuCtb GlPBe3U083ksEsKd3lktBBvV7a22ilYu8l3sbdXHFS1b3dVyUgpK1PX/f P6feCGwcFEvaubxiuyGKtPJibVVktt5rCamgOBh80lBzcGZwrjj9VfM5S Q==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 02/10] roadtest: add C backend Date: Fri, 11 Mar 2022 17:24:37 +0100 Message-ID: <20220311162445.346685-3-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Content-Type: text/plain; charset="utf-8" Add the C parts of the roadtest framework. This uses QEMU's libvhost-user to implement the device side of virtio-user and virtio-gpio and bridge them to the Python portions of the backend. The C backend is also responsible for starting UML after the virtio device implementations are initialized. Signed-off-by: Vincent Whitchurch --- tools/testing/roadtest/src/backend.c | 884 +++++++++++++++++++++++++++ 1 file changed, 884 insertions(+) create mode 100644 tools/testing/roadtest/src/backend.c diff --git a/tools/testing/roadtest/src/backend.c b/tools/testing/roadtest/= src/backend.c new file mode 100644 index 000000000000..d5ac08b20fd9 --- /dev/null +++ b/tools/testing/roadtest/src/backend.c @@ -0,0 +1,884 @@ +// SPDX-License-Identifier: GPL-2.0-only +// Copyright Axis Communications AB + +#define PY_SSIZE_T_CLEAN +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libvhost-user.h" + +enum watch_type { + LISTEN, + SOCKET_WATCH, + VU_WATCH, +}; + +struct watch { + VuDev *dev; + enum watch_type type; + int fd; + void *func; + void *data; + struct list_head list; +}; + +struct vhost_user_i2c { + VuDev dev; + FILE *control; +}; + +struct vhost_user_gpio { + VuDev dev; + FILE *control; + VuVirtqElement *irq_elements[64]; +}; + +#define dbg(...) = \ + do { \ + if (0) { \ + fprintf(stderr, __VA_ARGS__); \ + } \ + } while (0) + +static LIST_HEAD(watches); + +static int epfd; + +static PyObject *py_i2c_read, *py_i2c_write, *py_process_control; +static PyObject *py_gpio_set_irq_type, *py_gpio_unmask; + +static const char *opt_main_script; +static char *opt_gpio_socket; +static char *opt_i2c_socket; + +static struct vhost_user_gpio gpio; +static struct vhost_user_i2c i2c; + +static void dump_iov(const char *what, struct iovec *iovec, unsigned int c= ount) +{ + int i; + + dbg("dumping %s with count %u\n", what, count); + + for (i =3D 0; i < count; i++) { + struct iovec *iov =3D &iovec[0]; + + dbg("i %d base %p len %zu\n", i, iov->iov_base, iov->iov_len); + } +} + +static bool i2c_read(struct vhost_user_i2c *vi, uint16_t addr, void *data, + size_t len) +{ + PyObject *pArgs, *pValue; + + dbg("i2c read addr %#x len %zu\n", addr, len); + + pArgs =3D PyTuple_New(1); + pValue =3D PyLong_FromLong(len); + PyTuple_SetItem(pArgs, 0, pValue); + + pValue =3D PyObject_CallObject(py_i2c_read, pArgs); + Py_DECREF(pArgs); + if (!pValue) { + PyErr_Print(); + return false; + } + + unsigned char *buffer; + Py_ssize_t length; + + if (PyBytes_AsStringAndSize(pValue, (char **)&buffer, &length) < 0) { + PyErr_Print(); + errx(1, "invalid result from i2c.read()"); + } + if (length !=3D len) { + errx(1, + "unexpected length from i2c.read(), expected %zu, got %zu", + len, length); + } + + memcpy(data, buffer, len); + + return true; +} + +static bool i2c_write(struct vhost_user_i2c *vi, uint16_t addr, + const void *data, size_t len) +{ + PyObject *pArgs, *pValue; + + dbg("i2c write addr %#x len %zu\n", addr, len); + + pArgs =3D PyTuple_New(1); + pValue =3D PyBytes_FromStringAndSize(data, len); + PyTuple_SetItem(pArgs, 0, pValue); + + pValue =3D PyObject_CallObject(py_i2c_write, pArgs); + Py_DECREF(pArgs); + if (!pValue) { + PyErr_Print(); + return false; + } + + return true; +} + +static void gpio_send_irq_response(struct vhost_user_gpio *gpio, + unsigned int pin, unsigned int status); + +static PyObject *cbackend_trigger_gpio_irq(PyObject *self, PyObject *args) +{ + unsigned int pin; + + if (!PyArg_ParseTuple(args, "I", &pin)) + return NULL; + + dbg("trigger gpio %u irq\n", pin); + + gpio_send_irq_response(&gpio, pin, VIRTIO_GPIO_IRQ_STATUS_VALID); + + Py_RETURN_NONE; +} + +static PyMethodDef EmbMethods[] =3D { + { "trigger_gpio_irq", cbackend_trigger_gpio_irq, METH_VARARGS, + "Return the number of arguments received by the process." }, + { NULL, NULL, 0, NULL } +}; + +static PyModuleDef EmbModule =3D { PyModuleDef_HEAD_INIT, + "cbackend", + NULL, + -1, + EmbMethods, + NULL, + NULL, + NULL, + NULL }; + +static PyObject *PyInit_cbackend(void) +{ + return PyModule_Create(&EmbModule); +} + +static void init_python_i2c(PyObject *backend) +{ + PyObject *i2c =3D PyObject_GetAttrString(backend, "i2c"); + + if (!i2c) { + PyErr_Print(); + errx(1, "Error getting backend.i2c"); + } + + py_i2c_read =3D PyObject_GetAttrString(i2c, "read"); + if (!py_i2c_read) { + PyErr_Print(); + errx(1, "Error getting i2c.read"); + } + + py_i2c_write =3D PyObject_GetAttrString(i2c, "write"); + if (!py_i2c_write) { + PyErr_Print(); + errx(1, "Error getting i2c.write"); + } +} + +static void init_python_gpio(PyObject *backend) +{ + PyObject *gpio =3D PyObject_GetAttrString(backend, "gpio"); + + if (!gpio) { + PyErr_Print(); + errx(1, "error getting backend.gpio"); + } + + py_gpio_set_irq_type =3D PyObject_GetAttrString(gpio, "set_irq_type"); + if (!py_gpio_set_irq_type) { + PyErr_Print(); + errx(1, "error getting gpio.set_irq_type"); + } + + py_gpio_unmask =3D PyObject_GetAttrString(gpio, "unmask"); + if (!py_gpio_unmask) { + PyErr_Print(); + errx(1, "error getting gpio.unmask"); + } +} + +static void init_python(void) +{ + PyObject *mainmod, *backend; + FILE *file; + + PyImport_AppendInittab("cbackend", &PyInit_cbackend); + + Py_Initialize(); + + file =3D fopen(opt_main_script, "r"); + if (!file) + err(1, "open %s", opt_main_script); + + if (PyRun_SimpleFile(file, "main.py") < 0) { + PyErr_Print(); + errx(1, "error running %s", opt_main_script); + } + fclose(file); + + mainmod =3D PyImport_AddModule("__main__"); + if (!mainmod) { + PyErr_Print(); + errx(1, "error getting __main__"); + } + + backend =3D PyObject_GetAttrString(mainmod, "backend"); + if (!backend) { + PyErr_Print(); + errx(1, "error getting backend"); + } + + py_process_control =3D PyObject_GetAttrString(backend, "process_control"); + if (!py_process_control) { + PyErr_Print(); + errx(1, "error getting backend.process_control"); + } + + init_python_i2c(backend); + init_python_gpio(backend); +} + +static void i2c_handle_cmdq(VuDev *dev, int qidx) +{ + struct vhost_user_i2c *vi =3D + container_of(dev, struct vhost_user_i2c, dev); + VuVirtq *vq =3D vu_get_queue(dev, qidx); + VuVirtqElement *elem; + + for (;;) { + struct virtio_i2c_out_hdr *hdr; + struct iovec *resultv; + size_t used =3D 0; + bool ok =3D true; + + elem =3D vu_queue_pop(dev, vq, sizeof(VuVirtqElement)); + if (!elem) + break; + + dbg("elem %p index %u out_num %u in_num %u\n", elem, + elem->index, elem->out_num, elem->in_num); + dump_iov("out", elem->out_sg, elem->out_num); + dump_iov("in", elem->in_sg, elem->in_num); + + assert(elem->out_sg[0].iov_len =3D=3D sizeof(*hdr)); + hdr =3D elem->out_sg[0].iov_base; + + if (elem->out_num =3D=3D 2 && elem->in_num =3D=3D 1) { + struct iovec *data =3D &elem->out_sg[1]; + + ok =3D i2c_write(vi, hdr->addr, data->iov_base, + data->iov_len); + resultv =3D &elem->in_sg[0]; + } else if (elem->out_num =3D=3D 1 && elem->in_num =3D=3D 2) { + struct iovec *data =3D &elem->in_sg[0]; + + ok =3D i2c_read(vi, hdr->addr, data->iov_base, + data->iov_len); + resultv =3D &elem->in_sg[1]; + used +=3D data->iov_len; + } else { + assert(false); + } + + struct virtio_i2c_in_hdr *inhdr =3D resultv->iov_base; + + inhdr->status =3D ok ? VIRTIO_I2C_MSG_OK : VIRTIO_I2C_MSG_ERR; + + used +=3D sizeof(*inhdr); + vu_queue_push(dev, vq, elem, used); + free(elem); + } + + vu_queue_notify(&vi->dev, vq); +} + +static void i2c_queue_set_started(VuDev *dev, int qidx, bool started) +{ + VuVirtq *vq =3D vu_get_queue(dev, qidx); + + dbg("queue started %d:%d\n", qidx, started); + + vu_set_queue_handler(dev, vq, started ? i2c_handle_cmdq : NULL); +} + +static bool i2cquit; +static bool gpioquit; + +static void remove_watch(VuDev *dev, int fd); + +static int i2c_process_msg(VuDev *dev, VhostUserMsg *vmsg, int *do_reply) +{ + if (vmsg->request =3D=3D VHOST_USER_NONE) { + dbg("i2c disconnect"); + remove_watch(dev, -1); + i2cquit =3D true; + return true; + } + return false; +} +static int gpio_process_msg(VuDev *dev, VhostUserMsg *vmsg, int *do_reply) +{ + if (vmsg->request =3D=3D VHOST_USER_NONE) { + dbg("gpio disconnect"); + remove_watch(dev, -1); + gpioquit =3D true; + return true; + } + return false; +} + +static uint64_t i2c_get_features(VuDev *dev) +{ + return 1ull << VIRTIO_I2C_F_ZERO_LENGTH_REQUEST; +} + +static const VuDevIface i2c_iface =3D { + .get_features =3D i2c_get_features, + .queue_set_started =3D i2c_queue_set_started, + .process_msg =3D i2c_process_msg, +}; + +static void gpio_send_irq_response(struct vhost_user_gpio *gpio, + unsigned int pin, unsigned int status) +{ + assert(pin < ARRAY_SIZE(gpio->irq_elements)); + + VuVirtqElement *elem =3D gpio->irq_elements[pin]; + VuVirtq *vq =3D vu_get_queue(&gpio->dev, 1); + + if (!elem) { + dbg("no irq buf for pin %d\n", pin); + assert(status !=3D VIRTIO_GPIO_IRQ_STATUS_VALID); + return; + } + + struct virtio_gpio_irq_response *resp; + + assert(elem->out_num =3D=3D 1); + assert(elem->in_sg[0].iov_len =3D=3D sizeof(*resp)); + + resp =3D elem->in_sg[0].iov_base; + resp->status =3D status; + + vu_queue_push(&gpio->dev, vq, elem, sizeof(*resp)); + gpio->irq_elements[pin] =3D NULL; + free(elem); + + vu_queue_notify(&gpio->dev, vq); +} + +static void gpio_set_irq_type(struct vhost_user_gpio *gpio, unsigned int p= in, + unsigned int type) +{ + PyObject *pArgs, *pValue; + + pArgs =3D PyTuple_New(2); + pValue =3D PyLong_FromLong(pin); + PyTuple_SetItem(pArgs, 0, pValue); + + pValue =3D PyLong_FromLong(type); + PyTuple_SetItem(pArgs, 1, pValue); + + pValue =3D PyObject_CallObject(py_gpio_set_irq_type, pArgs); + if (!pValue) { + PyErr_Print(); + errx(1, "error from gpio.set_irq_type()"); + } + Py_DECREF(pArgs); + + if (type =3D=3D VIRTIO_GPIO_IRQ_TYPE_NONE) { + gpio_send_irq_response(gpio, pin, + VIRTIO_GPIO_IRQ_STATUS_INVALID); + } +} + +static void gpio_unmask(struct vhost_user_gpio *vi, unsigned int gpio) +{ + PyObject *pArgs, *pValue; + + pArgs =3D PyTuple_New(1); + pValue =3D PyLong_FromLong(gpio); + PyTuple_SetItem(pArgs, 0, pValue); + + pValue =3D PyObject_CallObject(py_gpio_unmask, pArgs); + if (!pValue) { + PyErr_Print(); + errx(1, "error from gpio.unmask()"); + } + Py_DECREF(pArgs); +} + +static void gpio_handle_cmdq(VuDev *dev, int qidx) +{ + struct vhost_user_gpio *vi =3D + container_of(dev, struct vhost_user_gpio, dev); + VuVirtq *vq =3D vu_get_queue(dev, qidx); + VuVirtqElement *elem; + + while (1) { + struct virtio_gpio_request *req; + struct virtio_gpio_response *resp; + + elem =3D vu_queue_pop(dev, vq, sizeof(VuVirtqElement)); + if (!elem) + break; + + dbg("elem %p index %u out_num %u in_num %u\n", elem, + elem->index, elem->out_num, elem->in_num); + + dump_iov("out", elem->out_sg, elem->out_num); + dump_iov("in", elem->in_sg, elem->in_num); + + assert(elem->out_num =3D=3D 1); + assert(elem->in_num =3D=3D 1); + + assert(elem->out_sg[0].iov_len =3D=3D sizeof(*req)); + assert(elem->in_sg[0].iov_len =3D=3D sizeof(*resp)); + + req =3D elem->out_sg[0].iov_base; + resp =3D elem->in_sg[0].iov_base; + + dbg("req type %#x gpio %#x value %#x\n", req->type, req->gpio, + req->value); + + switch (req->type) { + case VIRTIO_GPIO_MSG_IRQ_TYPE: + gpio_set_irq_type(vi, req->gpio, req->value); + break; + default: + /* + * The other types couldhooked up to Python later for + * testing of drivers' control of GPIOs. + */ + break; + } + + resp->status =3D VIRTIO_GPIO_STATUS_OK; + resp->value =3D 0; + + vu_queue_push(dev, vq, elem, sizeof(*resp)); + free(elem); + } + + vu_queue_notify(&vi->dev, vq); +} + +static void gpio_handle_eventq(VuDev *dev, int qidx) +{ + struct vhost_user_gpio *vi =3D + container_of(dev, struct vhost_user_gpio, dev); + VuVirtq *vq =3D vu_get_queue(dev, qidx); + VuVirtqElement *elem; + + for (;;) { + struct virtio_gpio_irq_request *req; + struct virtio_gpio_irq_response *resp; + + elem =3D vu_queue_pop(dev, vq, sizeof(VuVirtqElement)); + if (!elem) + break; + + dbg("elem %p index %u out_num %u in_num %u\n", elem, + elem->index, elem->out_num, elem->in_num); + + dump_iov("out", elem->out_sg, elem->out_num); + dump_iov("in", elem->in_sg, elem->in_num); + + assert(elem->out_num =3D=3D 1); + assert(elem->in_num =3D=3D 1); + + assert(elem->out_sg[0].iov_len =3D=3D sizeof(*req)); + assert(elem->in_sg[0].iov_len =3D=3D sizeof(*resp)); + + req =3D elem->out_sg[0].iov_base; + resp =3D elem->in_sg[0].iov_base; + + dbg("irq req gpio %#x\n", req->gpio); + + assert(req->gpio < ARRAY_SIZE(vi->irq_elements)); + assert(vi->irq_elements[req->gpio] =3D=3D NULL); + + vi->irq_elements[req->gpio] =3D elem; + + gpio_unmask(vi, req->gpio); + } +} + +static void gpio_queue_set_started(VuDev *dev, int qidx, bool started) +{ + VuVirtq *vq =3D vu_get_queue(dev, qidx); + + dbg("%s %d:%d\n", __func__, qidx, started); + + if (qidx =3D=3D 0) + vu_set_queue_handler(dev, vq, + started ? gpio_handle_cmdq : NULL); + if (qidx =3D=3D 1) + vu_set_queue_handler(dev, vq, + started ? gpio_handle_eventq : NULL); +} + +static int gpio_get_config(VuDev *dev, uint8_t *config, uint32_t len) +{ + struct vhost_user_gpio *gpio =3D + container_of(dev, struct vhost_user_gpio, dev); + static struct virtio_gpio_config gpioconfig =3D { + .ngpio =3D ARRAY_SIZE(gpio->irq_elements), + }; + + dbg("%s: len %u\n", __func__, len); + + if (len > sizeof(struct virtio_gpio_config)) + return -1; + + memcpy(config, &gpioconfig, len); + + return 0; +} + +static uint64_t gpio_get_protocol_features(VuDev *dev) +{ + return 1ull << VHOST_USER_PROTOCOL_F_CONFIG; +} + +static uint64_t gpio_get_features(VuDev *dev) +{ + return 1ull << VIRTIO_GPIO_F_IRQ; +} + +static const VuDevIface gpio_vuiface =3D { + .get_features =3D gpio_get_features, + .queue_set_started =3D gpio_queue_set_started, + .process_msg =3D gpio_process_msg, + .get_config =3D gpio_get_config, + .get_protocol_features =3D gpio_get_protocol_features, +}; + +static void panic(VuDev *dev, const char *err) +{ + fprintf(stderr, "panicking!"); + abort(); +} + +static struct watch *new_watch(struct VuDev *dev, int fd, enum watch_type = type, + void *func, void *data) +{ + struct watch *watch =3D malloc(sizeof(*watch)); + + assert(watch); + + watch->dev =3D dev; + watch->fd =3D fd; + watch->func =3D func; + watch->data =3D data; + watch->type =3D type; + + list_add(&watch->list, &watches); + + return watch; +} + +static void set_watch(VuDev *dev, int fd, int condition, vu_watch_cb cb, + void *data) +{ + struct watch *watch =3D new_watch(dev, fd, VU_WATCH, cb, data); + int ret; + + struct epoll_event ev =3D { + .events =3D EPOLLIN, + .data.ptr =3D watch, + }; + + dbg("set watch epfd %d fd %d condition %d cb %p\n", epfd, fd, condition, + cb); + + epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); + + ret =3D epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); + if (ret < 0) + err(1, "epoll_ctl"); +} + +static void remove_watch(VuDev *dev, int fd) +{ + struct watch *watch, *tmp; + + list_for_each_entry_safe(watch, tmp, &watches, list) { + if (watch->dev !=3D dev) + continue; + if (fd >=3D 0 && watch->fd !=3D fd) + continue; + + epoll_ctl(epfd, EPOLL_CTL_DEL, watch->fd, NULL); + + list_del(&watch->list); + free(watch); + } +} + +static int unix_listen(const char *path) +{ + struct sockaddr_un un =3D { + .sun_family =3D AF_UNIX, + }; + int sock; + int ret; + + unlink(path); + + sock =3D socket(PF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (sock < 0) + err(1, "socket"); + + memcpy(&un.sun_path, path, strlen(path)); + + ret =3D bind(sock, (struct sockaddr *)&un, sizeof(un)); + if (ret < 0) + err(1, "bind"); + + ret =3D listen(sock, 1); + if (ret < 0) + err(1, "listen"); + + return sock; +} + +static void dev_add_watch(int epfd, struct watch *watch) +{ + struct epoll_event event =3D { + .events =3D EPOLLIN | EPOLLONESHOT, + .data.ptr =3D watch, + }; + int ret; + + ret =3D epoll_ctl(epfd, EPOLL_CTL_ADD, watch->fd, &event); + if (ret < 0) + err(1, "EPOLL_CTL_ADD"); +} + +static VuDev *gpio_init(int epfd, const char *path) +{ + struct watch *watch; + VuDev *dev; + int lsock; + bool rc; + + lsock =3D unix_listen(path); + if (lsock < 0) + err(1, "listen %s", path); + + rc =3D vu_init(&gpio.dev, 2, lsock, panic, NULL, set_watch, + remove_watch, &gpio_vuiface); + assert(rc =3D=3D true); + + dev =3D &gpio.dev; + watch =3D new_watch(dev, lsock, LISTEN, vu_dispatch, dev); + + dev_add_watch(epfd, watch); + + return dev; +} + +static VuDev *i2c_init(int epfd, const char *path) +{ + static struct vhost_user_i2c i2c =3D {}; + VuDev *dev =3D &i2c.dev; + struct watch *watch; + int lsock; + bool rc; + + lsock =3D unix_listen(path); + if (lsock < 0) + err(1, "listen %s", path); + + rc =3D vu_init(dev, 1, lsock, panic, NULL, set_watch, + remove_watch, &i2c_iface); + assert(rc =3D=3D true); + + watch =3D new_watch(dev, lsock, LISTEN, vu_dispatch, dev); + + dev_add_watch(epfd, watch); + + return dev; +} + +static pid_t run_uml(char **argv) +{ + int log, null, ret; + pid_t pid; + + pid =3D fork(); + if (pid < 0) + err(1, "fork"); + if (pid > 0) + return pid; + + chdir(getenv("ROADTEST_WORK_DIR")); + + log =3D open("uml.txt", O_WRONLY | O_TRUNC | O_APPEND | O_CREAT, 0600); + if (log < 0) + err(1, "open uml.txt"); + + null =3D open("/dev/null", O_RDONLY); + if (null < 0) + err(1, "open null"); + + ret =3D dup2(null, 0); + if (ret < 0) + err(1, "dup2"); + + ret =3D dup2(log, 1); + if (ret < 0) + err(1, "dup2"); + + ret =3D dup2(log, 2); + if (ret < 0) + err(1, "dup2"); + + execvpe(argv[0], argv, environ); + err(1, "execve"); + + return -1; +} + +int main(int argc, char *argv[]) +{ + static struct option long_option[] =3D { + { "main-script", required_argument, 0, 'm' }, + { "gpio-socket", required_argument, 0, 'g' }, + { "i2c-socket", required_argument, 0, 'i' }, + }; + + while (1) { + int c =3D getopt_long(argc, argv, "", long_option, NULL); + + if (c =3D=3D -1) + break; + + switch (c) { + case 'm': + opt_main_script =3D optarg; + break; + + case 'g': + opt_gpio_socket =3D optarg; + break; + + case 'i': + opt_i2c_socket =3D optarg; + break; + + default: + errx(1, "getopt"); + } + } + + if (!opt_main_script || !opt_gpio_socket || !opt_i2c_socket) + errx(1, "Invalid arguments"); + + epfd =3D epoll_create1(EPOLL_CLOEXEC); + if (epfd < 0) + err(1, "epoll_create1"); + + init_python(); + + gpio_init(epfd, opt_gpio_socket); + i2c_init(epfd, opt_i2c_socket); + + run_uml(&argv[optind]); + + while (1) { + struct epoll_event events[10]; + int nfds; + int i; + + nfds =3D epoll_wait(epfd, events, ARRAY_SIZE(events), -1); + if (nfds < 0) { + if (errno =3D=3D EINTR) { + continue; + + err(1, "epoll_wait"); + } + } + + if (!PyObject_CallObject(py_process_control, NULL)) { + PyErr_Print(); + errx(1, "error from backend.process_control"); + } + + for (i =3D 0; i < nfds; i++) { + struct epoll_event *event =3D &events[i]; + struct watch *watch =3D event->data.ptr; + int fd; + + switch (watch->type) { + case LISTEN: + fd =3D accept(watch->fd, NULL, NULL); + close(watch->fd); + if (fd =3D=3D -1) + err(1, "accept"); + + watch->dev->sock =3D fd; + watch->fd =3D fd; + watch->type =3D SOCKET_WATCH; + + struct epoll_event event =3D { + .events =3D EPOLLIN, + .data.ptr =3D watch, + }; + + int ret =3D epoll_ctl(epfd, EPOLL_CTL_ADD, fd, + &event); + if (ret < 0) + err(1, "epoll_ctl"); + + break; + case SOCKET_WATCH: + vu_dispatch(watch->dev); + break; + case VU_WATCH: + ((vu_watch_cb)(watch->func))(watch->dev, POLLIN, + watch->data); + break; + default: + fprintf(stderr, "abort!"); + abort(); + } + } + + if (i2cquit && gpioquit) + break; + } + + vu_deinit(&i2c.dev); + vu_deinit(&gpio.dev); + + Py_Finalize(); + + return 0; +} --=20 2.34.1 From nobody Tue Jun 23 03:06:51 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id B0F28C433F5 for ; Fri, 11 Mar 2022 16:25:46 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1350340AbiCKQ0s (ORCPT ); Fri, 11 Mar 2022 11:26:48 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:40788 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350319AbiCKQ0d (ORCPT ); Fri, 11 Mar 2022 11:26:33 -0500 Received: from smtp2.axis.com (smtp2.axis.com [195.60.68.18]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id E71EB10A7C5; Fri, 11 Mar 2022 08:24:49 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015894; x=1678551894; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=WEyVVHDFtozxXKM8qFJKp+Cn8U7LcuaL3JvjOEc4jok=; b=MIbQkHhl9OsaPBA5Zr/CYrVbTxDQB9mYOyu3ZHnSKtJnfd1A7SIW/CW+ 5hkHPoSGpvZ18CFHoCIKiU/B02zEZiZ/jx2F2Rzw660qIxYvat1PZ588f zi9zaQFI66ZjLMP83qXSYwhbhrr+ZlPAunjf1uIJ2XHbLUj3hiSAT0Vam 1DIeD0MlxfXzBxmpuWsyuZUUb+yEZ+pHDjyyN0G99bLWrRvrUy4fAAax1 XuYem7ezSEqRxHs5Jky5AoZIV6Ds4eMXvF1gZpNp7lFni3sGpDZk1Szsf PJjrESBhazzFUa9Qtsao6LNHGU42BfXDp/YYxynNMlo/9HTs2TRsDV1m0 A==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 03/10] roadtest: add framework Date: Fri, 11 Mar 2022 17:24:38 +0100 Message-ID: <20220311162445.346685-4-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Content-Type: text/plain; charset="utf-8" Add the bulk of the roadtest framework. Apart from one init shell script, this is written in Python and includes three closely-related parts: - The test runner which is invoked from the command line by the user and which starts the backend and sends the test jobs and results to/from UML. - Test support code which is used by the actual driver tests run inside UML and which interact with the backend via a file-based asynchronous communication method. - The backend which is run by the Python interpreter embedded in the C backend. This part runs the hardware models and is controlled by the tests and the driver (via virtio in the C backend). Some unit tests for the framework itself are included and these will be automatically run whenever the driver tests are run. Signed-off-by: Vincent Whitchurch --- tools/testing/roadtest/init.sh | 19 ++ tools/testing/roadtest/roadtest/__init__.py | 2 + .../roadtest/roadtest/backend/__init__.py | 0 .../roadtest/roadtest/backend/backend.py | 32 ++ .../testing/roadtest/roadtest/backend/gpio.py | 111 +++++++ .../testing/roadtest/roadtest/backend/i2c.py | 123 ++++++++ .../testing/roadtest/roadtest/backend/main.py | 13 + .../testing/roadtest/roadtest/backend/mock.py | 20 ++ .../roadtest/roadtest/backend/test_gpio.py | 98 ++++++ .../roadtest/roadtest/backend/test_i2c.py | 84 +++++ .../testing/roadtest/roadtest/cmd/__init__.py | 0 tools/testing/roadtest/roadtest/cmd/main.py | 146 +++++++++ tools/testing/roadtest/roadtest/cmd/remote.py | 48 +++ .../roadtest/roadtest/core/__init__.py | 0 .../testing/roadtest/roadtest/core/control.py | 52 ++++ .../roadtest/roadtest/core/devicetree.py | 155 ++++++++++ .../roadtest/roadtest/core/hardware.py | 94 ++++++ tools/testing/roadtest/roadtest/core/log.py | 42 +++ .../testing/roadtest/roadtest/core/modules.py | 38 +++ .../testing/roadtest/roadtest/core/opslog.py | 35 +++ tools/testing/roadtest/roadtest/core/proxy.py | 48 +++ tools/testing/roadtest/roadtest/core/suite.py | 286 ++++++++++++++++++ tools/testing/roadtest/roadtest/core/sysfs.py | 77 +++++ .../roadtest/roadtest/core/test_control.py | 35 +++ .../roadtest/roadtest/core/test_devicetree.py | 31 ++ .../roadtest/roadtest/core/test_hardware.py | 41 +++ .../roadtest/roadtest/core/test_log.py | 54 ++++ .../roadtest/roadtest/core/test_opslog.py | 27 ++ .../roadtest/roadtest/tests/__init__.py | 0 29 files changed, 1711 insertions(+) create mode 100755 tools/testing/roadtest/init.sh create mode 100644 tools/testing/roadtest/roadtest/__init__.py create mode 100644 tools/testing/roadtest/roadtest/backend/__init__.py create mode 100644 tools/testing/roadtest/roadtest/backend/backend.py create mode 100644 tools/testing/roadtest/roadtest/backend/gpio.py create mode 100644 tools/testing/roadtest/roadtest/backend/i2c.py create mode 100644 tools/testing/roadtest/roadtest/backend/main.py create mode 100644 tools/testing/roadtest/roadtest/backend/mock.py create mode 100644 tools/testing/roadtest/roadtest/backend/test_gpio.py create mode 100644 tools/testing/roadtest/roadtest/backend/test_i2c.py create mode 100644 tools/testing/roadtest/roadtest/cmd/__init__.py create mode 100644 tools/testing/roadtest/roadtest/cmd/main.py create mode 100644 tools/testing/roadtest/roadtest/cmd/remote.py create mode 100644 tools/testing/roadtest/roadtest/core/__init__.py create mode 100644 tools/testing/roadtest/roadtest/core/control.py create mode 100644 tools/testing/roadtest/roadtest/core/devicetree.py create mode 100644 tools/testing/roadtest/roadtest/core/hardware.py create mode 100644 tools/testing/roadtest/roadtest/core/log.py create mode 100644 tools/testing/roadtest/roadtest/core/modules.py create mode 100644 tools/testing/roadtest/roadtest/core/opslog.py create mode 100644 tools/testing/roadtest/roadtest/core/proxy.py create mode 100644 tools/testing/roadtest/roadtest/core/suite.py create mode 100644 tools/testing/roadtest/roadtest/core/sysfs.py create mode 100644 tools/testing/roadtest/roadtest/core/test_control.py create mode 100644 tools/testing/roadtest/roadtest/core/test_devicetree.py create mode 100644 tools/testing/roadtest/roadtest/core/test_hardware.py create mode 100644 tools/testing/roadtest/roadtest/core/test_log.py create mode 100644 tools/testing/roadtest/roadtest/core/test_opslog.py create mode 100644 tools/testing/roadtest/roadtest/tests/__init__.py diff --git a/tools/testing/roadtest/init.sh b/tools/testing/roadtest/init.sh new file mode 100755 index 000000000000..c5fb28478aa3 --- /dev/null +++ b/tools/testing/roadtest/init.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-only + +mount -t proc proc /proc +echo 8 > /proc/sys/kernel/printk +mount -t sysfs nodev /sys +mount -t debugfs nodev /sys/kernel/debug + +echo 0 > /sys/bus/i2c/drivers_autoprobe +echo 0 > /sys/bus/platform/drivers_autoprobe + +python3 -m roadtest.cmd.remote +status=3D$? +[ "${ROADTEST_SHELL}" =3D "1" ] || { + # rsync doesn't handle these zero-sized files correctly. + cp -ra --no-preserve=3Downership /sys/kernel/debug/gcov ${ROADTEST_WOR= K_DIR}/gcov + echo o > /proc/sysrq-trigger +} +exec setsid sh -c 'exec bash /dev/tty0 2>&1' diff --git a/tools/testing/roadtest/roadtest/__init__.py b/tools/testing/ro= adtest/roadtest/__init__.py new file mode 100644 index 000000000000..dac3ce6976e5 --- /dev/null +++ b/tools/testing/roadtest/roadtest/__init__.py @@ -0,0 +1,2 @@ +ENV_WORK_DIR =3D "ROADTEST_WORK_DIR" +ENV_BUILD_DIR =3D "ROADTEST_BUILD_DIR" diff --git a/tools/testing/roadtest/roadtest/backend/__init__.py b/tools/te= sting/roadtest/roadtest/backend/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/testing/roadtest/roadtest/backend/backend.py b/tools/tes= ting/roadtest/roadtest/backend/backend.py new file mode 100644 index 000000000000..bfd19fc363c2 --- /dev/null +++ b/tools/testing/roadtest/roadtest/backend/backend.py @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import logging +import os +from pathlib import Path + +from roadtest import ENV_WORK_DIR +from roadtest.core.control import ControlReader + +from . import gpio, i2c, mock + +logger =3D logging.getLogger(__name__) + +try: + import cbackend # type: ignore[import] +except ModuleNotFoundError: + # In unit tests + cbackend =3D None + + +class Backend: + def __init__(self) -> None: + work =3D Path(os.environ[ENV_WORK_DIR]) + self.control =3D ControlReader(work_dir=3Dwork) + self.c =3D cbackend + self.i2c =3D i2c.I2CBackend(self) + self.gpio =3D gpio.GpioBackend(self) + self.mock =3D mock.MockBackend(work) + + def process_control(self) -> None: + self.control.process({"backend": self}) diff --git a/tools/testing/roadtest/roadtest/backend/gpio.py b/tools/testin= g/roadtest/roadtest/backend/gpio.py new file mode 100644 index 000000000000..2eaf52b31c72 --- /dev/null +++ b/tools/testing/roadtest/roadtest/backend/gpio.py @@ -0,0 +1,111 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import logging +import typing +from typing import Optional + +if typing.TYPE_CHECKING: + # Avoid circular imports + from .backend import Backend + +logger =3D logging.getLogger(__name__) + + +class Gpio: + IRQ_TYPE_NONE =3D 0x00 + IRQ_TYPE_EDGE_RISING =3D 0x01 + IRQ_TYPE_EDGE_FALLING =3D 0x02 + IRQ_TYPE_EDGE_BOTH =3D 0x03 + IRQ_TYPE_LEVEL_HIGH =3D 0x04 + IRQ_TYPE_LEVEL_LOW =3D 0x08 + + def __init__(self, backend: "Backend", pin: int): + self.backend =3D backend + self.pin =3D pin + self.state =3D False + self.irq_type =3D Gpio.IRQ_TYPE_NONE + self.masked =3D True + self.edge_irq_latched =3D False + + def _level_irq_active(self) -> bool: + if self.irq_type =3D=3D Gpio.IRQ_TYPE_LEVEL_HIGH: + return self.state + elif self.irq_type =3D=3D Gpio.IRQ_TYPE_LEVEL_LOW: + return not self.state + + return False + + def _latch_edge_irq(self, old: bool, new: bool) -> bool: + if old !=3D new: + logger.debug(f"{self}: latch_edge_irq {self.irq_type} {old} ->= {new}") + + if self.irq_type =3D=3D Gpio.IRQ_TYPE_EDGE_RISING: + return not old and new + elif self.irq_type =3D=3D Gpio.IRQ_TYPE_EDGE_FALLING: + return old and not new + elif self.irq_type =3D=3D Gpio.IRQ_TYPE_EDGE_BOTH: + return old !=3D new + + return False + + def _check_irq(self) -> None: + if self.irq_type =3D=3D Gpio.IRQ_TYPE_NONE or self.masked: + return + if not self.edge_irq_latched and not self._level_irq_active(): + return + + self.masked =3D True + self.edge_irq_latched =3D False + + logger.debug(f"{self}: trigger irq") + self.backend.c.trigger_gpio_irq(self.pin) + + def set_irq_type(self, irq_type: int) -> None: + logger.debug(f"{self}: set_irq_type {irq_type}") + if irq_type =3D=3D Gpio.IRQ_TYPE_NONE: + self.masked =3D True + + self.irq_type =3D irq_type + self.edge_irq_latched =3D False + self._check_irq() + + def unmask(self) -> None: + logger.debug(f"{self}: unmask") + self.masked =3D False + self._check_irq() + + def set(self, val: int) -> None: + old =3D self.state + new =3D bool(val) + + if old !=3D new: + logger.debug(f"{self}: gpio set {old} -> {new}") + + self.state =3D new + if self._latch_edge_irq(old, new): + logger.debug(f"{self}: latching edge") + self.edge_irq_latched =3D True + + self._check_irq() + + def __str__(self) -> str: + return f"Gpio({self.pin})" + + +class GpioBackend: + def __init__(self, backend: "Backend") -> None: + self.backend =3D backend + self.gpios =3D [Gpio(backend, pin) for pin in range(64)] + + def set(self, pin: Optional[int], val: bool) -> None: + if pin is None: + return + + self.gpios[pin].set(val) + + def set_irq_type(self, pin: int, irq_type: int) -> None: + self.gpios[pin].set_irq_type(irq_type) + + def unmask(self, pin: int) -> None: + self.gpios[pin].unmask() diff --git a/tools/testing/roadtest/roadtest/backend/i2c.py b/tools/testing= /roadtest/roadtest/backend/i2c.py new file mode 100644 index 000000000000..b877c2b76851 --- /dev/null +++ b/tools/testing/roadtest/roadtest/backend/i2c.py @@ -0,0 +1,123 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import abc +import importlib +import logging +import typing +from typing import Any, Literal, Optional + +if typing.TYPE_CHECKING: + # Avoid circular imports + from .backend import Backend + +logger =3D logging.getLogger(__name__) + + +class I2CBackend: + def __init__(self, backend: "Backend") -> None: + self.model: Optional[I2CModel] =3D None + self.backend =3D backend + + def load_model(self, modname: str, clsname: str, *args: Any, **kwargs:= Any) -> None: + mod =3D importlib.import_module(modname) + cls =3D getattr(mod, clsname) + self.model =3D cls(*args, **kwargs, backend=3Dself.backend) + + def unload_model(self) -> None: + self.model =3D None + + def read(self, length: int) -> bytes: + if not self.model: + raise Exception("No I2C model loaded") + + return self.model.read(length) + + def write(self, data: bytes) -> None: + if not self.model: + raise Exception("No I2C model loaded") + + self.model.write(data) + + def __getattr__(self, name: str) -> Any: + return getattr(self.model, name) + + +class I2CModel(abc.ABC): + def __init__(self, backend: "Backend") -> None: + self.backend =3D backend + + @abc.abstractmethod + def read(self, length: int) -> bytes: + return bytes(length) + + @abc.abstractmethod + def write(self, data: bytes) -> None: + pass + + +class SMBusModel(I2CModel): + def __init__( + self, + regbytes: int, + byteorder: Literal["little", "big"] =3D "little", + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + self.reg_addr =3D 0x0 + self.regbytes =3D regbytes + self.byteorder =3D byteorder + + @abc.abstractmethod + def reg_read(self, addr: int) -> int: + return 0 + + @abc.abstractmethod + def reg_write(self, addr: int, val: int) -> None: + pass + + def val_to_bytes(self, val: int) -> bytes: + return val.to_bytes(self.regbytes, self.byteorder) + + def bytes_to_val(self, data: bytes) -> int: + return int.from_bytes(data, self.byteorder) + + def read(self, length: int) -> bytes: + data =3D bytearray() + for idx in range(0, length, self.regbytes): + addr =3D self.reg_addr + idx + val =3D self.reg_read(addr) + logger.debug(f"SMBus read {addr=3D:#02x} {val=3D:#02x}") + data +=3D self.val_to_bytes(val) + return bytes(data) + + def write(self, data: bytes) -> None: + self.reg_addr =3D data[0] + + if len(data) > 1: + length =3D len(data) - 1 + data =3D data[1:] + assert length % self.regbytes =3D=3D 0 + for idx in range(0, length, self.regbytes): + val =3D self.bytes_to_val(data[idx : (idx + self.regbytes)= ]) + addr =3D self.reg_addr + idx + self.backend.mock.reg_write(addr, val) + self.reg_write(addr, val) + logger.debug(f"SMBus write {addr=3D:#02x} {val=3D:#02x}") + elif len(data) =3D=3D 1: + pass + + +class SimpleSMBusModel(SMBusModel): + def __init__(self, regs: dict[int, int], **kwargs: Any) -> None: + super().__init__(**kwargs) + self.regs =3D regs + + def reg_read(self, addr: int) -> int: + val =3D self.regs[addr] + return val + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + self.regs[addr] =3D val diff --git a/tools/testing/roadtest/roadtest/backend/main.py b/tools/testin= g/roadtest/roadtest/backend/main.py new file mode 100644 index 000000000000..25be86ded9ea --- /dev/null +++ b/tools/testing/roadtest/roadtest/backend/main.py @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import logging + +import roadtest.backend.backend + +logging.basicConfig( + format=3D"%(asctime)s - %(levelname)s - %(name)s: %(message)s", level= =3Dlogging.DEBUG +) + +backend =3D roadtest.backend.backend.Backend() +backend.process_control() diff --git a/tools/testing/roadtest/roadtest/backend/mock.py b/tools/testin= g/roadtest/roadtest/backend/mock.py new file mode 100644 index 000000000000..8ce33a6bc0f1 --- /dev/null +++ b/tools/testing/roadtest/roadtest/backend/mock.py @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import functools +from pathlib import Path +from typing import Any, Callable + +from roadtest.core.opslog import OpsLogWriter + + +class MockBackend: + def __init__(self, work: Path) -> None: + self.opslog =3D OpsLogWriter(work) + + @functools.cache + def __getattr__(self, name: str) -> Callable: + def func(*args: Any, **kwargs: Any) -> None: + self.opslog.write(f"mock.{name}(*{str(args)}, **{str(kwargs)})= ") + + return func diff --git a/tools/testing/roadtest/roadtest/backend/test_gpio.py b/tools/t= esting/roadtest/roadtest/backend/test_gpio.py new file mode 100644 index 000000000000..feffe4fb9625 --- /dev/null +++ b/tools/testing/roadtest/roadtest/backend/test_gpio.py @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import unittest +from unittest.mock import MagicMock + +from .gpio import Gpio + + +class TestGpio(unittest.TestCase): + def test_irq_low(self) -> None: + m =3D MagicMock() + gpio =3D Gpio(backend=3Dm, pin=3D1) + + gpio.set_irq_type(Gpio.IRQ_TYPE_LEVEL_LOW) + m.c.trigger_gpio_irq.assert_not_called() + + gpio.unmask() + m.c.trigger_gpio_irq.assert_called_once_with(1) + m.c.trigger_gpio_irq.reset_mock() + + gpio.set(True) + gpio.unmask() + m.c.trigger_gpio_irq.assert_not_called() + + def test_irq_high(self) -> None: + m =3D MagicMock() + gpio =3D Gpio(backend=3Dm, pin=3D2) + + gpio.set_irq_type(Gpio.IRQ_TYPE_LEVEL_HIGH) + gpio.unmask() + + m.c.trigger_gpio_irq.assert_not_called() + + gpio.set(True) + m.c.trigger_gpio_irq.assert_called_once_with(2) + m.c.trigger_gpio_irq.reset_mock() + + gpio.set(False) + gpio.unmask() + m.c.trigger_gpio_irq.assert_not_called() + + def test_irq_rising(self) -> None: + m =3D MagicMock() + gpio =3D Gpio(backend=3Dm, pin=3D63) + + gpio.set_irq_type(Gpio.IRQ_TYPE_EDGE_RISING) + gpio.set(False) + gpio.set(True) + + m.c.trigger_gpio_irq.assert_not_called() + gpio.unmask() + m.c.trigger_gpio_irq.assert_called_once_with(63) + m.c.trigger_gpio_irq.reset_mock() + + gpio.set(False) + gpio.set(True) + + gpio.unmask() + m.c.trigger_gpio_irq.assert_called_once() + + def test_irq_falling(self) -> None: + m =3D MagicMock() + gpio =3D Gpio(backend=3Dm, pin=3D0) + + gpio.set_irq_type(Gpio.IRQ_TYPE_EDGE_FALLING) + gpio.unmask() + gpio.set(False) + gpio.set(True) + m.c.trigger_gpio_irq.assert_not_called() + + gpio.set(False) + m.c.trigger_gpio_irq.assert_called_once_with(0) + m.c.trigger_gpio_irq.reset_mock() + + gpio.set(True) + gpio.set(False) + gpio.set(True) + gpio.unmask() + m.c.trigger_gpio_irq.assert_called_once() + + def test_irq_both(self) -> None: + m =3D MagicMock() + gpio =3D Gpio(backend=3Dm, pin=3D32) + + gpio.set_irq_type(Gpio.IRQ_TYPE_EDGE_BOTH) + gpio.unmask() + gpio.set(False) + gpio.set(True) + m.c.trigger_gpio_irq.assert_called_once_with(32) + + gpio.set(False) + m.c.trigger_gpio_irq.assert_called_once_with(32) + m.c.trigger_gpio_irq.reset_mock() + + gpio.set(True) + gpio.unmask() + m.c.trigger_gpio_irq.assert_called_once_with(32) diff --git a/tools/testing/roadtest/roadtest/backend/test_i2c.py b/tools/te= sting/roadtest/roadtest/backend/test_i2c.py new file mode 100644 index 000000000000..eda4e1a4b80f --- /dev/null +++ b/tools/testing/roadtest/roadtest/backend/test_i2c.py @@ -0,0 +1,84 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import unittest +from typing import Any +from unittest.mock import MagicMock + +from .i2c import SimpleSMBusModel, SMBusModel + + +class DummyModel(SMBusModel): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.regs: dict[int, int] =3D {} + + def reg_read(self, addr: int) -> int: + return self.regs[addr] + + def reg_write(self, addr: int, val: int) -> None: + self.regs[addr] =3D val + + +class TestSMBusModel(unittest.TestCase): + def test_1(self) -> None: + m =3D DummyModel(regbytes=3D1, backend=3DMagicMock()) + + m.write(bytes([0x12, 0x34])) + m.write(bytes([0x13, 0xAB, 0xCD])) + + self.assertEqual(m.regs[0x12], 0x34) + self.assertEqual(m.regs[0x13], 0xAB) + self.assertEqual(m.regs[0x14], 0xCD) + + m.write(bytes([0x12])) + self.assertEqual(m.read(1), bytes([0x34])) + + m.write(bytes([0x12])) + self.assertEqual(m.read(3), bytes([0x34, 0xAB, 0xCD])) + + def test_2big(self) -> None: + m =3D DummyModel(regbytes=3D2, byteorder=3D"big", backend=3DMagicM= ock()) + + m.write(bytes([0x12, 0x34, 0x56, 0xAB, 0xCD])) + self.assertEqual(m.regs[0x12], 0x3456) + self.assertEqual(m.regs[0x14], 0xABCD) + + m.write(bytes([0x12])) + self.assertEqual(m.read(2), bytes([0x34, 0x56])) + + m.write(bytes([0x14])) + self.assertEqual(m.read(2), bytes([0xAB, 0xCD])) + + m.write(bytes([0x12])) + self.assertEqual(m.read(4), bytes([0x34, 0x56, 0xAB, 0xCD])) + + def test_2little(self) -> None: + m =3D DummyModel(regbytes=3D2, byteorder=3D"little", backend=3DMag= icMock()) + + m.write(bytes([0x12, 0x34, 0x56, 0xAB, 0xCD])) + self.assertEqual(m.regs[0x12], 0x5634) + self.assertEqual(m.regs[0x14], 0xCDAB) + + m.write(bytes([0x12])) + self.assertEqual(m.read(2), bytes([0x34, 0x56])) + + +class TestSimpleSMBusModel(unittest.TestCase): + def test_simple(self) -> None: + m =3D SimpleSMBusModel( + regs=3D{0x01: 0x12, 0x02: 0x34}, + regbytes=3D1, + backend=3DMagicMock(), + ) + self.assertEqual(m.reg_read(0x01), 0x12) + self.assertEqual(m.reg_read(0x02), 0x34) + + m.reg_write(0x01, 0x56) + self.assertEqual(m.reg_read(0x01), 0x56) + self.assertEqual(m.reg_read(0x02), 0x34) + + with self.assertRaises(Exception): + m.reg_write(0x03, 0x00) + with self.assertRaises(Exception): + m.reg_read(0x03) diff --git a/tools/testing/roadtest/roadtest/cmd/__init__.py b/tools/testin= g/roadtest/roadtest/cmd/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/testing/roadtest/roadtest/cmd/main.py b/tools/testing/ro= adtest/roadtest/cmd/main.py new file mode 100644 index 000000000000..634c27fe795c --- /dev/null +++ b/tools/testing/roadtest/roadtest/cmd/main.py @@ -0,0 +1,146 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import argparse +import fnmatch +import sys +import unittest +from typing import Optional +from unittest.suite import TestSuite + +assert sys.version_info >=3D (3, 9), "Python version is too old" + +from roadtest.core.suite import UMLSuite, UMLTestCase + + +def make_umlsuite(args: argparse.Namespace) -> UMLSuite: + return UMLSuite( + timeout=3Dargs.timeout, + workdir=3Dargs.work_dir, + builddir=3Dargs.build_dir, + ksrcdir=3Dargs.ksrc_dir, + uml_args_pre=3Dargs.uml_prepend, + uml_args_post=3Dargs.uml_append, + shell=3Dargs.shell, + ) + + +def main() -> None: + parser =3D argparse.ArgumentParser() + parser.add_argument( + "--timeout", + type=3Dint, + default=3D60, + help=3D"Timeout (in seconds) for each UML run, 0 to disable", + ) + parser.add_argument("--work-dir", type=3Dstr, help=3D"Work directory f= or UML runs") + parser.add_argument("--build-dir", type=3Dstr, required=3DTrue) + parser.add_argument("--ksrc-dir", type=3Dstr, required=3DTrue) + parser.add_argument( + "--uml-prepend", + nargs=3D"*", + default=3D[], + help=3D"Extra arguments to prepend to the UML command (example: gd= bserver :1234)", + ) + parser.add_argument( + "--uml-append", + nargs=3D"*", + default=3D[], + help=3D"Extra arguments to append to the UML command (example: tra= ce_event=3Di2c:* tp_printk)", + ) + parser.add_argument( + "--filter", + nargs=3D"+", + default=3D[], + ) + parser.add_argument("--shell", action=3D"store_true") + parser.add_argument("test", nargs=3D"?", default=3D"roadtest") + args =3D parser.parse_args() + + if args.shell: + args.timeout =3D 0 + + if not any(p.startswith("con=3D") for p in args.uml_append): + print( + "Error: --shell used but no con=3D UML argument specified", + file=3Dsys.stderr, + ) + sys.exit(1) + + test =3D args.test + test =3D test.replace("/", ".") + test =3D test.removesuffix(".py") + test =3D test.removesuffix(".") + + loader =3D unittest.defaultTestLoader + suitegroups =3D loader.discover(test) + + args.filter =3D [f"*{f}*" for f in args.filter] + + # Backend tests and the like don't need to be run inside UML. + localsuite =3D None + + # For simplicity, we currently run all target tests in one UML instance + # since python in UML is slow to start up. This can be revisited if we + # want to run several UML instances in parallel. + deftargetsuite =3D None + targetsuites =3D [] + + for suites in suitegroups: + # unittest can in arbitrarily nest and mix TestCases + # and TestSuites, but we expect a fixed hierarchy. + assert isinstance(suites, unittest.TestSuite) + + for suite in suites: + # assert not isinstance(suite, unittest.TestCase) + + # If the import of a test fails, then suite is a + # unittest.loader._FailedTest instead of a suite + if not isinstance(suite, unittest.TestSuite): + suite =3D [suite] # type: ignore[assignment] + + # Suite at this level contains one TestCase for each + # test method in a particular test class. + # + # All the test functions for one particular test class + # can only be run either in UML or locally, not mixed. + destsuite: Optional[TestSuite] =3D None + + for t in suite: # type: ignore[union-attr] + # We don't support suites nested at this level. + assert isinstance(t, unittest.TestCase) + + id =3D t.id() + if args.filter and not any(fnmatch.fnmatch(id, f) for f in= args.filter): + continue + + if isinstance(t, UMLTestCase): + if t.run_separately: + if not destsuite: + destsuite =3D make_umlsuite(args) + targetsuites.append(destsuite) + else: + if not deftargetsuite: + deftargetsuite =3D make_umlsuite(args) + targetsuites.append(deftargetsuite) + + destsuite =3D deftargetsuite + else: + if not localsuite: + localsuite =3D TestSuite() + destsuite =3D localsuite + + if destsuite: + destsuite.addTest(t) + + tests =3D unittest.TestSuite() + if localsuite: + tests.addTest(localsuite) + tests.addTests(targetsuites) + + result =3D unittest.TextTestRunner(verbosity=3D2).run(tests) + sys.exit(not result.wasSuccessful()) + + +if __name__ =3D=3D "__main__": + main() diff --git a/tools/testing/roadtest/roadtest/cmd/remote.py b/tools/testing/= roadtest/roadtest/cmd/remote.py new file mode 100644 index 000000000000..29c3c6d35c65 --- /dev/null +++ b/tools/testing/roadtest/roadtest/cmd/remote.py @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import importlib +import json +import os +from pathlib import Path +from typing import cast +from unittest import TestSuite, TextTestRunner + +from roadtest import ENV_WORK_DIR +from roadtest.core import proxy + + +def main() -> None: + workdir =3D Path(os.environ[ENV_WORK_DIR]) + with open(workdir / "tests.json") as f: + testinfos =3D json.load(f) + + suite =3D TestSuite() + for info in testinfos: + id =3D info["id"] + *modparts, clsname, method =3D id.split(".") + + fullname =3D ".".join(modparts) + mod =3D importlib.import_module(fullname) + + cls =3D getattr(mod, clsname) + test =3D cls(methodName=3Dmethod) + + values =3D info["values"] + if values: + test.dts.values =3D values + + suite.addTest(test) + + runner =3D TextTestRunner( + verbosity=3D0, buffer=3DFalse, resultclass=3Dproxy.ProxyTextTestRe= sult + ) + result =3D cast(proxy.ProxyTextTestResult, runner.run(suite)) + + proxyresult =3D result.to_proxy() + with open(workdir / "results.json", "w") as f: + json.dump(proxyresult, f) + + +if __name__ =3D=3D "__main__": + main() diff --git a/tools/testing/roadtest/roadtest/core/__init__.py b/tools/testi= ng/roadtest/roadtest/core/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/testing/roadtest/roadtest/core/control.py b/tools/testin= g/roadtest/roadtest/core/control.py new file mode 100644 index 000000000000..cd74861099b9 --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/control.py @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import logging +import os +from pathlib import Path +from typing import Optional + +from roadtest import ENV_WORK_DIR + +CONTROL_FILE =3D "control.txt" + +logger =3D logging.getLogger(__name__) + + +class ControlReader: + def __init__(self, work_dir: Optional[Path] =3D None) -> None: + if not work_dir: + work_dir =3D Path(os.environ[ENV_WORK_DIR]) + + path =3D work_dir / CONTROL_FILE + path.unlink(missing_ok=3DTrue) + path.write_text("") + + self.file =3D path.open("r") + + def process(self, vars: dict) -> None: + for line in self.file.readlines(): + cmd =3D line.rstrip() + + if cmd.startswith("# "): + logger.info(line[2:].rstrip()) + continue + + logger.debug(cmd) + eval(cmd, vars) + + +class ControlWriter: + def __init__(self, work_dir: Optional[Path] =3D None) -> None: + if not work_dir: + work_dir =3D Path(os.environ[ENV_WORK_DIR]) + self.file =3D (work_dir / CONTROL_FILE).open("a", buffering=3D1) + + def write_cmd(self, line: str) -> None: + self.file.write(line + "\n") + + def write_log(self, line: str) -> None: + self.file.write(f"# {line}\n") + + def close(self) -> None: + self.file.close() diff --git a/tools/testing/roadtest/roadtest/core/devicetree.py b/tools/tes= ting/roadtest/roadtest/core/devicetree.py new file mode 100644 index 000000000000..40876738fb39 --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/devicetree.py @@ -0,0 +1,155 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import enum +import subprocess +from pathlib import Path +from typing import Any, Optional + +HEADER =3D """ +/dts-v1/; + +/ { + #address-cells =3D <2>; + #size-cells =3D <2>; + + virtio@0 { + compatible =3D "virtio,uml"; + socket-path =3D "WORK/gpio.sock"; + virtio-device-id =3D <0x29>; + + gpio: gpio { + compatible =3D "virtio,device29"; + + gpio-controller; + #gpio-cells =3D <2>; + + interrupt-controller; + #interrupt-cells =3D <2>; + }; + }; + + virtio@1 { + compatible =3D "virtio,uml"; + socket-path =3D "WORK/i2c.sock"; + virtio-device-id =3D <0x22>; + + i2c: i2c { + compatible =3D "virtio,device22"; + + #address-cells =3D <1>; + #size-cells =3D <0>; + }; + }; + + // See Hardware.kick() + leds { + compatible =3D "gpio-leds"; + led0 { + gpios =3D <&gpio 0 0>; + }; + }; +}; +""" + + +class DtVar(enum.Enum): + I2C_ADDR =3D 0 + GPIO_PIN =3D 1 + + +class DtFragment: + def __init__(self, src: str, variables: Optional[dict[str, DtVar]] =3D= None) -> None: + self.src =3D src + if not variables: + variables =3D {} + self.variables =3D variables + self.values: dict[str, int] =3D {} + + def apply(self, values: dict[str, Any]) -> str: + src =3D self.src + + for var in self.variables.keys(): + typ =3D self.variables[var] + val =3D values[var] + + if typ =3D=3D DtVar.I2C_ADDR: + str =3D f"{val:02x}" + elif typ =3D=3D DtVar.GPIO_PIN: + str =3D f"{val:d}" + + src =3D src.replace(f"${var}$", str) + + self.values =3D values + return src + + def __getitem__(self, key: str) -> Any: + return self.values[key] + + +class Devicetree: + def __init__(self, workdir: Path, ksrcdir: Path) -> None: + self.workdir: Path =3D workdir + self.ksrcdir: Path =3D ksrcdir + self.next_i2c_addr: int =3D 0x1 + # 0 is used for gpio-leds for Hardware.kick() + self.next_gpio_pin: int =3D 1 + self.src: str =3D "" + + def assemble(self, fragments: list[DtFragment]) -> None: + parts =3D [] + for fragment in fragments: + if fragment.values: + # Multiple test functions from the same class will use + # the same class instance + continue + + values =3D {} + + for var, type in fragment.variables.items(): + if type =3D=3D DtVar.I2C_ADDR: + values[var] =3D self.next_i2c_addr + self.next_i2c_addr +=3D 1 + elif type =3D=3D DtVar.GPIO_PIN: + values[var] =3D self.next_gpio_pin + self.next_gpio_pin +=3D 1 + + parts.append(fragment.apply(values)) + + self.src =3D "\n".join(parts) + + def compile(self, dtb: str) -> None: + dts =3D self.workdir / "test.dts" + + try: + subprocess.run( + [ + "gcc", + "-E", + "-nostdinc", + f"-I{self.ksrcdir}/scripts/dtc/include-prefixes", + "-undef", + "-D__DTS__", + "-x", + "assembler-with-cpp", + "-o", + dts, + "-", + ], + input=3Dself.src, + text=3DTrue, + check=3DTrue, + capture_output=3DTrue, + ) + + full =3D HEADER.replace("WORK", str(self.workdir)) + dts.read_= text() + dts.write_text(full) + + subprocess.run( + ["dtc", "-I", "dts", "-O", "dtb", dts, "-o", self.workdir = / dtb], + check=3DTrue, + capture_output=3DTrue, + text=3DTrue, + ) + except subprocess.CalledProcessError as e: + raise Exception(f"{e.stderr}") diff --git a/tools/testing/roadtest/roadtest/core/hardware.py b/tools/testi= ng/roadtest/roadtest/core/hardware.py new file mode 100644 index 000000000000..ae81a531d2a2 --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/hardware.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import contextlib +import functools +import os +from pathlib import Path +from typing import Any, Callable, Optional, Type, cast +from unittest import TestCase +from unittest.mock import MagicMock, call + +from roadtest import ENV_WORK_DIR + +from .control import ControlWriter +from .opslog import OpsLogReader +from .sysfs import write_int + + +class HwMock(MagicMock): + def assert_reg_write_once(self, test: TestCase, reg: int, value: int) = -> None: + test.assertEqual( + [c for c in self.mock_calls if c.args[0] =3D=3D reg], + [call.reg_write(reg, value)], + ) + + def assert_last_reg_write(self, test: TestCase, reg: int, value: int) = -> None: + test.assertEqual( + [c for c in self.mock_calls if c.args[0] =3D=3D reg][-1:], + [call.reg_write(reg, value)], + ) + + def get_last_reg_write(self, reg: int) -> int: + return cast(int, [c for c in self.mock_calls if c.args[0] =3D=3D r= eg][-1].args[1]) + + +class Hardware(contextlib.AbstractContextManager): + def __init__(self, bus: str, work: Optional[Path] =3D None) -> None: + if not work: + work =3D Path(os.environ[ENV_WORK_DIR]) + + self.bus =3D bus + self.mock =3D HwMock() + self.control =3D ControlWriter(work) + self.opslog =3D OpsLogReader(work) + self.loaded_model =3D False + + # Ignore old entries + self.opslog.read_next() + + def _call(self, method: str, *args: Any, **kwargs: Any) -> None: + self.control.write_cmd( + f"backend.{self.bus}.{method}(*{str(args)}, **{str(kwargs)})" + ) + + def kick(self) -> None: + # Control writes are only applied when the backend gets something + # to process, usually because the driver tried to access the devic= e. + # But in some cases, such as when the driver is waiting for a + # sequence of interrupts, the test code needs the control write to= take + # effect immediately. For this, we just need to kick the backend + # into processing its control queue. + # + # We (ab)use gpio-leds for this. devicetree.py sets up the device. + write_int(Path("/sys/class/leds/led0/brightness"), 0) + + def load_model(self, cls: Type[Any], *args: Any, **kwargs: Any) -> "Ha= rdware": + self._call("load_model", cls.__module__, cls.__name__, *args, **kw= args) + self.loaded_model =3D True + return self + + def __enter__(self) -> "Hardware": + return self + + def __exit__(self, *_: Any) -> None: + self.close() + + @functools.cache + def __getattr__(self, name: str) -> Callable: + def func(*args: Any, **kwargs: Any) -> None: + self._call(name, *args, **kwargs) + + return func + + def close(self) -> None: + if self.loaded_model: + self._call("unload_model") + self.control.close() + + def update_mock(self) -> HwMock: + opslog =3D self.opslog.read_next() + for line in opslog: + eval(line, {"mock": self.mock}) + + return self.mock diff --git a/tools/testing/roadtest/roadtest/core/log.py b/tools/testing/ro= adtest/roadtest/core/log.py new file mode 100644 index 000000000000..7d73e40eb2d8 --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/log.py @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +from pathlib import Path + + +class LogParser: + DNF_MESSAGE =3D "" + + def __init__(self, file: Path): + try: + raw =3D file.read_text() + lines =3D raw.splitlines() + except FileNotFoundError: + lines =3D [] + raw =3D "" + + self.raw =3D raw + self.lines =3D lines + + def has_any(self) -> bool: + return "START<" in self.raw + + def get_testcase_log(self, id: str) -> list[str]: + startmarker =3D f"START<{id}>" + stopmarker =3D f"STOP<{id}>" + + try: + startpos =3D next( + i for i, line in enumerate(self.lines) if startmarker in l= ine + ) + except StopIteration: + return [] + + try: + stoppos =3D next( + i for i, line in enumerate(self.lines[startpos:]) if stopm= arker in line + ) + except StopIteration: + return self.lines[startpos + 1 :] + [LogParser.DNF_MESSAGE] + + return self.lines[startpos + 1 : startpos + stoppos] diff --git a/tools/testing/roadtest/roadtest/core/modules.py b/tools/testin= g/roadtest/roadtest/core/modules.py new file mode 100644 index 000000000000..5bd2d92a322b --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/modules.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import os +import subprocess +from pathlib import Path +from typing import Any + +from roadtest import ENV_BUILD_DIR + + +def modprobe(modname: str, remove: bool =3D False) -> None: + moddir =3D Path(os.environ[ENV_BUILD_DIR]) / "modules" + args =3D [] + if remove: + args.append("--remove") + args +=3D [f"--dirname=3D{moddir}", modname] + subprocess.check_output(["/sbin/modprobe"] + args) + + +def insmod(modname: str) -> None: + modprobe(modname) + + +def rmmod(modname: str) -> None: + subprocess.check_output(["/sbin/rmmod", modname]) + + +class Module: + def __init__(self, name: str) -> None: + self.name =3D name + + def __enter__(self) -> "Module": + modprobe(self.name) + return self + + def __exit__(self, *_: Any) -> None: + rmmod(self.name) diff --git a/tools/testing/roadtest/roadtest/core/opslog.py b/tools/testing= /roadtest/roadtest/core/opslog.py new file mode 100644 index 000000000000..83bb4f525d03 --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/opslog.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import os +from pathlib import Path + +OPSLOG_FILE =3D "opslog.txt" + + +class OpsLogWriter: + def __init__(self, work: Path) -> None: + path =3D work / OPSLOG_FILE + path.unlink(missing_ok=3DTrue) + self.file =3D open(path, "a", buffering=3D1) + + def write(self, line: str) -> None: + self.file.write(line + "\n") + + +class OpsLogReader: + def __init__(self, work: Path) -> None: + self.path =3D work / OPSLOG_FILE + self.opslogpos =3D 0 + + def read_next(self) -> list[str]: + # There is a problem in hostfs (see Hostfs Caveats) which means + # that reads from UML on a file which is extended on the host don'= t see + # the new data unless we open and close the file, so we can't open= once + # and use readlines(). + with open(self.path, "r") as f: + os.lseek(f.fileno(), self.opslogpos, os.SEEK_SET) + opslog =3D [line.rstrip() for line in f.readlines()] + self.opslogpos =3D os.lseek(f.fileno(), 0, os.SEEK_CUR) + + return opslog diff --git a/tools/testing/roadtest/roadtest/core/proxy.py b/tools/testing/= roadtest/roadtest/core/proxy.py new file mode 100644 index 000000000000..36089e21d7d5 --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/proxy.py @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +from typing import Any +from unittest import TestCase, TextTestResult + +from . import control + + +class ProxyTextTestResult(TextTestResult): + def __init__(self, stream: Any, descriptions: Any, verbosity: Any) -> = None: + super().__init__(stream, descriptions, verbosity) + self.successes: list[tuple[TestCase, str]] =3D [] + + # Print via kmsg to avoid getting cut off by other kernel prints. + self.kmsg =3D open("/dev/kmsg", "w", buffering=3D1) + self.control =3D control.ControlWriter() + + def addSuccess(self, test: TestCase) -> None: + super().addSuccess(test) + self.successes.append((test, "")) + + def _log(self, test: TestCase, action: str) -> None: + line =3D f"{action}<{test.id()}>" + self.kmsg.write(line + "\n") + self.control.write_log(line) + + def startTest(self, test: TestCase) -> None: + self._log(test, "START") + super().startTest(test) + + def stopTest(self, test: TestCase) -> None: + super().stopTest(test) + self._log(test, "STOP") + + def _replace_id(self, reslist: list[tuple[TestCase, str]]) -> list[tup= le[str, str]]: + return [(case.id(), tb) for case, tb in reslist] + + def to_proxy(self) -> dict[str, Any]: + return { + "testsRun": self.testsRun, + "wasSuccessful": self.wasSuccessful(), + "successes": self._replace_id(self.successes), + "errors": self._replace_id(self.errors), + "failures": self._replace_id(self.failures), + "skipped": self._replace_id(self.skipped), + "unexpectedSuccesses": [t.id() for t in self.unexpectedSuccess= es], + } diff --git a/tools/testing/roadtest/roadtest/core/suite.py b/tools/testing/= roadtest/roadtest/core/suite.py new file mode 100644 index 000000000000..e99a60b4faba --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/suite.py @@ -0,0 +1,286 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import json +import os +import shlex +import signal +import subprocess +import textwrap +import unittest +from pathlib import Path +from typing import Any, ClassVar, Optional, Tuple, cast +from unittest import TestResult + +from roadtest import ENV_BUILD_DIR, ENV_WORK_DIR + +from . import devicetree +from .log import LogParser + + +class UMLTestCase(unittest.TestCase): + run_separately: ClassVar[bool] =3D False + dts: ClassVar[Optional[devicetree.DtFragment]] =3D None + + +class UMLSuite(unittest.TestSuite): + def __init__( + self, + timeout: int, + workdir: str, + builddir: str, + ksrcdir: str, + uml_args_pre: list[str], + uml_args_post: list[str], + shell: bool, + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + + self.timeout =3D timeout + self.workdir =3D Path(workdir).resolve() + self.builddir =3D Path(builddir) + self.ksrcdir =3D Path(ksrcdir) + self.uml_args_pre =3D uml_args_pre + self.uml_args_post =3D uml_args_post + self.shell =3D shell + + self.backendlog =3D self.workdir / "backend.txt" + self.umllog =3D self.workdir / "uml.txt" + + # Used from the roadtest.cmd.remote running inside UML + self.testfile =3D self.workdir / "tests.json" + self.resultfile =3D self.workdir / "results.json" + + def run( + self, result: unittest.TestResult, debug: bool =3D False + ) -> unittest.TestResult: + pwd =3D os.getcwd() + + os.makedirs(self.workdir, exist_ok=3DTrue) + workdir =3D self.workdir + + tests =3D cast(list[UMLTestCase], list(self)) + + os.environ[ENV_WORK_DIR] =3D str(workdir) + os.environ[ENV_BUILD_DIR] =3D str(self.builddir) + + dt =3D devicetree.Devicetree(workdir=3Dworkdir, ksrcdir=3Dself.ksr= cdir) + dt.assemble([test.dts for test in tests if test.dts]) + dt.compile("test.dtb") + + testinfos =3D [] + ids =3D [] + for t in tests: + id =3D t.id() + # This fixup is needed when discover is done starting from "ro= adtest" + if not id.startswith("roadtest."): + id =3D f"roadtest.{id}" + ids.append(id) + + testinfos.append({"id": id, "values": t.dts.values if t.dts el= se {}}) + + with self.testfile.open("w") as f: + json.dump(testinfos, f) + + uml_args =3D [ + str(self.builddir / "vmlinux"), + f"PYTHONPATH=3D{pwd}", + f"{ENV_WORK_DIR}=3D{workdir}", + f"{ENV_BUILD_DIR}=3D{self.builddir}", + # Should be enough for anybody? + "mem=3D64M", + "dtb=3Dtest.dtb", + "rootfstype=3Dhostfs", + "rw", + f"init=3D{pwd}/init.sh", + f"uml_dir=3D{workdir}", + "umid=3Duml", + # ProxyTextTestResult writes to /dev/kmsg + "printk.devkmsg=3Don", + "slub_debug", + # For ease of debugging + "no_hash_pointers", + ] + + if self.shell: + # See init.sh + uml_args +=3D ["ROADTEST_SHELL=3D1"] + else: + # Set by slub_debug + TAINT_BAD_PAGE =3D 1 << 5 + uml_args +=3D [ + # init.sh increases the loglevel after bootup. + "quiet", + "panic_on_warn=3D1", + f"panic_on_taint=3D{TAINT_BAD_PAGE}", + "oops=3Dpanic", + # Speeds up delays, but as a consequence also causes + # 100% CPU consumption at an idle shell prompt. + "time-travel", + ] + + main_script =3D (Path(__file__).parent / "../backend/main.py").res= olve() + + args =3D ( + [ + str(self.builddir / "roadtest-backend"), + # The socket locations are also present in the devicetree. + f"--gpio-socket=3D{workdir}/gpio.sock", + f"--i2c-socket=3D{workdir}/i2c.sock", + f"--main-script=3D{main_script}", + "--", + ] + + self.uml_args_pre + + uml_args + + self.uml_args_post + ) + + print( + "Running backend/UML with: {}".format( + " ".join([shlex.quote(a) for a in args]) + ) + ) + + # Truncate instead of deleting so that tail -f can be used to moni= tor + # the log across runs. + self.backendlog.write_text("") + self.umllog.write_text("") + self.resultfile.unlink(missing_ok=3DTrue) + + umlpidfile =3D workdir / "uml/pid" + umlpidfile.unlink(missing_ok=3DTrue) + + newenv =3D dict(os.environ, PYTHONPATH=3Dpwd) + + try: + process =3D None + with self.backendlog.open("w") as f: + process =3D subprocess.Popen( + args, + env=3Dnewenv, + stdin=3Dsubprocess.PIPE, + stdout=3Df, + stderr=3Dsubprocess.STDOUT, + text=3DTrue, + preexec_fn=3Dos.setsid, + ) + process.wait(self.timeout if self.timeout else None) + except subprocess.TimeoutExpired: + pass + finally: + try: + if process: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + try: + pid =3D int(umlpidfile.read_text()) + os.killpg(pid, signal.SIGKILL) + except (FileNotFoundError, ProcessLookupError): + pass + + if process and process.returncode is not None and process.returnco= de !=3D 0: + with self.backendlog.open("a") as f: + f.write(f"\n") + + try: + with self.resultfile.open("r") as f: + proxy =3D json.load(f) + except FileNotFoundError: + # UML crashed, timed out, etc + proxy =3D None + + return self._convert_results(proxy, tests, result) + + def _parse_status(self, id: str, proxy: dict) -> Tuple[str, str]: + if not proxy: + return "ERROR", "No result. UML or backend crashed?\n" + + try: + _, tb =3D next(e for e in proxy["successes"] if e[0] =3D=3D id) + return "ok", "" + except StopIteration: + pass + + try: + _, tb =3D next(e for e in proxy["errors"] if e[0] =3D=3D id) + return "ERROR", tb + except StopIteration: + pass + + try: + _, tb =3D next(e for e in proxy["failures"] if e[0] =3D=3D id) + return "FAIL", tb + except StopIteration: + pass + + # setupClass, etc + if proxy["errors"]: + _, tb =3D proxy["errors"][0] + return "ERROR", tb + + raise Exception("Unable to parse status") + + def _get_log( + self, name: str, parser: LogParser, id: str, full_if_none: bool + ) -> Optional[str]: + testloglines =3D parser.get_testcase_log(id) + tb =3D None + if testloglines: + tb =3D "\n".join([f"{name} log:"] + [" " + line for line in te= stloglines]) + elif full_if_none and not parser.has_any(): + if parser.raw: + tb =3D "\n".join( + [f"Full {name} log:", textwrap.indent(parser.raw, " ")= .rstrip()] + ) + else: + tb =3D f"\nNo {name} log found." + + return tb + + def _convert_results( + self, + proxy: dict, + tests: list[UMLTestCase], + result: TestResult, + ) -> TestResult: + umllog =3D LogParser(self.umllog) + backendlog =3D LogParser(self.backendlog) + + first_fail =3D True + for test in tests: + assert isinstance(test, unittest.TestCase) + + id =3D test.id() + if not id.startswith("roadtest."): + id =3D f"roadtest.{id}" + + status, tb =3D self._parse_status(id, proxy) + if status !=3D "ok": + parts =3D [] + + backendtb =3D self._get_log("Backend", backendlog, id, fir= st_fail) + if backendtb: + parts.append(backendtb) + + umltb =3D self._get_log("UML", umllog, id, first_fail) + if umltb: + parts.append(umltb) + + # In the case of no START/STOP markers at all in the logs,= we include + # the full logs, but only do it in the first failing test = case to + # reduce noise. + first_fail =3D False + tb =3D "\n\n".join(parts + [tb]) + + if status =3D=3D "ERROR": + result.errors.append((test, tb)) + elif status =3D=3D "FAIL": + result.failures.append((test, tb)) + + print(f"{test} ... {status}") + result.testsRun +=3D 1 + + return result diff --git a/tools/testing/roadtest/roadtest/core/sysfs.py b/tools/testing/= roadtest/roadtest/core/sysfs.py new file mode 100644 index 000000000000..64228978718e --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/sysfs.py @@ -0,0 +1,77 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import contextlib +from pathlib import Path +from typing import Iterator + + +# Path.write_text() is inappropriate since Python calls write(2) +# a second time if the first one returns an error, if the file +# was opened as text. +def write_str(path: Path, val: str) -> None: + path.write_bytes(val.encode()) + + +def write_int(path: Path, val: int) -> None: + write_str(path, str(val)) + + +def write_float(path: Path, val: float) -> None: + write_str(path, str(val)) + + +def read_str(path: Path) -> str: + return path.read_text().rstrip() + + +def read_int(path: Path) -> int: + return int(read_str(path)) + + +def read_float(path: Path) -> float: + return float(read_str(path)) + + +class I2CDevice: + def __init__(self, addr: int, bus: int =3D 0) -> None: + self.id =3D f"{bus}-{addr:04x}" + self.path =3D Path(f"/sys/bus/i2c/devices/{self.id}") + + +class PlatformDevice: + def __init__(self, name: str) -> None: + self.id =3D name + self.path =3D Path(f"/sys/bus/platform/devices/{self.id}") + + +class I2CDriver: + def __init__(self, driver: str) -> None: + self.driver =3D driver + self.path =3D Path(f"/sys/bus/i2c/drivers/{driver}") + + @contextlib.contextmanager + def bind(self, addr: int, bus: int =3D 0) -> Iterator[I2CDevice]: + dev =3D I2CDevice(addr, bus) + write_str(self.path / "bind", dev.id) + + try: + yield dev + finally: + write_str(self.path / "unbind", dev.id) + + +class PlatformDriver: + def __init__(self, driver: str) -> None: + self.driver =3D driver + self.path =3D Path(f"/sys/bus/platform/drivers/{driver}") + + @contextlib.contextmanager + def bind(self, addr: str) -> Iterator[PlatformDevice]: + dev =3D PlatformDevice(addr) + write_str(self.path / "bind", dev.id) + + try: + yield dev + finally: + write_str(self.path / "unbind", dev.id) diff --git a/tools/testing/roadtest/roadtest/core/test_control.py b/tools/t= esting/roadtest/roadtest/core/test_control.py new file mode 100644 index 000000000000..a8cf9105eb52 --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/test_control.py @@ -0,0 +1,35 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest import TestCase + +from .control import ControlReader, ControlWriter + + +class TestControl(TestCase): + def test_control(self) -> None: + with TemporaryDirectory() as tmpdir: + work =3D Path(tmpdir) + reader =3D ControlReader(work) + writer =3D ControlWriter(work) + + values =3D [] + + def append(new: int) -> None: + nonlocal values + values.append(new) + + vars =3D {"append": append} + writer.write_cmd("append(1)") + + reader.process(vars) + self.assertEqual(values, [1]) + + writer.write_cmd("append(2)") + writer.write_log("append(4)") + writer.write_cmd("append(3)") + + reader.process(vars) + self.assertEqual(values, [1, 2, 3]) diff --git a/tools/testing/roadtest/roadtest/core/test_devicetree.py b/tool= s/testing/roadtest/roadtest/core/test_devicetree.py new file mode 100644 index 000000000000..db61fd24b39a --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/test_devicetree.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import tempfile +import unittest +from pathlib import Path + +from . import devicetree + + +class TestDevicetree(unittest.TestCase): + def test_compile(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + tmpdir =3D Path(tmp) + # We don't have the ksrcdir so we can't test if includes work. + dt =3D devicetree.Devicetree(tmpdir, tmpdir) + + dt.assemble( + [ + devicetree.DtFragment( + src=3D""" +&i2c { + foo =3D <1>; +}; + """ + ) + ] + ) + dt.compile("test.dtb") + dtb =3D tmpdir / "test.dtb" + self.assertTrue((dtb).exists()) diff --git a/tools/testing/roadtest/roadtest/core/test_hardware.py b/tools/= testing/roadtest/roadtest/core/test_hardware.py new file mode 100644 index 000000000000..eb09b317e258 --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/test_hardware.py @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest import TestCase + +from roadtest.backend.mock import MockBackend + +from .hardware import Hardware + + +class TestHardware(TestCase): + def test_mock(self) -> None: + with TemporaryDirectory() as tmpdir: + work =3D Path(tmpdir) + + backend =3D MockBackend(work) + hw =3D Hardware(bus=3D"dummy", work=3Dwork) + + backend.reg_write(0x1, 0xDEAD) + backend.reg_write(0x2, 0xBEEF) + mock =3D hw.update_mock() + mock.assert_reg_write_once(self, 0x1, 0xDEAD) + + backend.reg_write(0x1, 0xCAFE) + mock =3D hw.update_mock() + with self.assertRaises(AssertionError): + mock.assert_reg_write_once(self, 0x1, 0xDEAD) + + mock.assert_last_reg_write(self, 0x1, 0xCAFE) + + self.assertEqual(mock.get_last_reg_write(0x1), 0xCAFE) + self.assertEqual(mock.get_last_reg_write(0x2), 0xBEEF) + + with self.assertRaises(IndexError): + self.assertEqual(mock.get_last_reg_write(0x3), 0x0) + + mock.reset_mock() + with self.assertRaises(AssertionError): + mock.assert_last_reg_write(self, 0x2, 0xBEEF) diff --git a/tools/testing/roadtest/roadtest/core/test_log.py b/tools/testi= ng/roadtest/roadtest/core/test_log.py new file mode 100644 index 000000000000..6988ff4419db --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/test_log.py @@ -0,0 +1,54 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +from pathlib import Path +from tempfile import NamedTemporaryFile +from unittest import TestCase + +from .log import LogParser + + +class TestLog(TestCase): + def test_parser(self) -> None: + with NamedTemporaryFile() as tmpfile: + path =3D Path(tmpfile.name) + + path.write_text( + """ +xyz START +finished1 +finished2 +STOP +START +STOP +START monkey STOP +START +unfinished1 +unfinished2""" + ) + + parser =3D LogParser(path) + self.assertEqual( + parser.get_testcase_log("finished"), ["finished1", "finish= ed2"] + ) + + self.assertEqual( + parser.get_testcase_log("unfinished"), + ["unfinished1", "unfinished2", LogParser.DNF_MESSAGE], + ) + + self.assertEqual( + parser.get_testcase_log("notpresent"), + [], + ) + + self.assertEqual( + parser.get_testcase_log("enpty"), + [], + ) + + # Shouldn't happen since we print from the kernel? + self.assertEqual( + parser.get_testcase_log("foo"), + [], + ) diff --git a/tools/testing/roadtest/roadtest/core/test_opslog.py b/tools/te= sting/roadtest/roadtest/core/test_opslog.py new file mode 100644 index 000000000000..bd594c587032 --- /dev/null +++ b/tools/testing/roadtest/roadtest/core/test_opslog.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest import TestCase + +from .opslog import OpsLogReader, OpsLogWriter + + +class TestOpsLOg(TestCase): + def test_opslog(self) -> None: + with TemporaryDirectory() as tmpdir: + work =3D Path(tmpdir) + writer =3D OpsLogWriter(work) + reader =3D OpsLogReader(work) + + self.assertEqual(reader.read_next(), []) + + writer.write("1") + writer.write("2") + + self.assertEqual(reader.read_next(), ["1", "2"]) + self.assertEqual(reader.read_next(), []) + + writer.write("3") + self.assertEqual(reader.read_next(), ["3"]) diff --git a/tools/testing/roadtest/roadtest/tests/__init__.py b/tools/test= ing/roadtest/roadtest/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 --=20 2.34.1 From nobody Tue Jun 23 03:06:51 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 240C4C43217 for ; Fri, 11 Mar 2022 16:26:15 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1350443AbiCKQ1Q (ORCPT ); Fri, 11 Mar 2022 11:27:16 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:40924 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350271AbiCKQ0h (ORCPT ); Fri, 11 Mar 2022 11:26:37 -0500 Received: from smtp1.axis.com (smtp1.axis.com [195.60.68.17]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 094111BF930; Fri, 11 Mar 2022 08:25:07 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015909; x=1678551909; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=a4jqKhMbsN8UJ3i116koA51lQKLvtPhDM9o1oRoAbiw=; b=ID5Ud+pNpe7qiXGAX1K2P14GHr/qtfMPrmNNrLT8T+EK5n6s8erXWLTS 6oKvZAnrueSIGy/qHi6/fc5sEmX+t7iIdHtvCa8XWrVUT1LONsErISc0X nbD+hXPd/l8BIOzRRm4iKLzj3nXETpLf3RvOCK/+0D4re8T6SPPTjuRmB QYYRpLh0ekRPTdP3orTgEX7iWd75kGfl+gAjaDikXIeXQ0r7RbQ/IzUjz fsNUjLw5iFyCeRnU+fcl1OYS+vPyJ0A+i9kWY+vloBO8hAayXTmC28m5q Li+VY47M1hCfqBrX22hc0COVp5FcXFjDURFpqitSIzfeJErsbU0DJMJrc Q==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 04/10] roadtest: add base config Date: Fri, 11 Mar 2022 17:24:39 +0100 Message-ID: <20220311162445.346685-5-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Content-Type: text/plain; charset="utf-8" Add the base config options for the roadtest kernel (generated with "savedefconfig"). roadtest uses a single kernel for all tests and the drivers under test are built as modules. Additional config options are added by merging config fragments from each subsystems' test directory. The kernel is built with several debug options to catch more problems during testing. Signed-off-by: Vincent Whitchurch --- .../roadtest/roadtest/tests/base/config | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 tools/testing/roadtest/roadtest/tests/base/config diff --git a/tools/testing/roadtest/roadtest/tests/base/config b/tools/test= ing/roadtest/roadtest/tests/base/config new file mode 100644 index 000000000000..c1952d047c8e --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/base/config @@ -0,0 +1,84 @@ +CONFIG_NO_HZ=3Dy +CONFIG_HIGH_RES_TIMERS=3Dy +CONFIG_LOG_BUF_SHIFT=3D14 +CONFIG_EXPERT=3Dy +CONFIG_HOSTFS=3Dy +CONFIG_UML_TIME_TRAVEL_SUPPORT=3Dy +CONFIG_NULL_CHAN=3Dy +CONFIG_PORT_CHAN=3Dy +CONFIG_PTY_CHAN=3Dy +CONFIG_TTY_CHAN=3Dy +CONFIG_XTERM_CHAN=3Dy +CONFIG_CON_CHAN=3D"pts" +CONFIG_SSL=3Dy +CONFIG_SSL_CHAN=3D"pts" +CONFIG_MAGIC_SYSRQ=3Dy +CONFIG_VIRTIO_UML=3Dy +CONFIG_UML_PCI_OVER_VIRTIO=3Dy +CONFIG_UML_PCI_OVER_VIRTIO_DEVICE_ID=3D1234 +CONFIG_GCOV_KERNEL=3Dy +CONFIG_MODULES=3Dy +CONFIG_MODULE_UNLOAD=3Dy +CONFIG_BINFMT_MISC=3Dm +# CONFIG_COMPACTION is not set +CONFIG_DEVTMPFS=3Dy +CONFIG_DEVTMPFS_MOUNT=3Dy +CONFIG_OF=3Dy +# CONFIG_INPUT is not set +CONFIG_LEGACY_PTY_COUNT=3D32 +CONFIG_HW_RANDOM=3Dy +# CONFIG_HW_RANDOM_IXP4XX is not set +# CONFIG_HW_RANDOM_STM32 is not set +# CONFIG_HW_RANDOM_MESON is not set +# CONFIG_HW_RANDOM_CAVIUM is not set +# CONFIG_HW_RANDOM_MTK is not set +# CONFIG_HW_RANDOM_EXYNOS is not set +# CONFIG_HW_RANDOM_NPCM is not set +# CONFIG_HW_RANDOM_KEYSTONE is not set +CONFIG_RANDOM_TRUST_BOOTLOADER=3Dy +CONFIG_I2C=3Dy +# CONFIG_I2C_COMPAT is not set +CONFIG_I2C_CHARDEV=3Dy +CONFIG_I2C_VIRTIO=3Dy +CONFIG_I2C_STUB=3Dm +CONFIG_PPS=3Dy +CONFIG_GPIOLIB=3Dy +CONFIG_GPIO_VIRTIO=3Dy +CONFIG_NET=3Dy +CONFIG_UNIX=3Dy +CONFIG_NEW_LEDS=3Dy +CONFIG_LEDS_CLASS=3Dy +CONFIG_LEDS_GPIO=3Dy +CONFIG_LEDS_TRIGGERS=3Dy +CONFIG_LEDS_TRIGGER_HEARTBEAT=3Dy +CONFIG_RTC_CLASS=3Dy +# CONFIG_RTC_HCTOSYS is not set +# CONFIG_RTC_SYSTOHC is not set +CONFIG_RTC_DEBUG=3Dy +# CONFIG_RTC_NVMEM is not set +CONFIG_VIRTIO_INPUT=3Dy +# CONFIG_BCM_VIDEOCORE is not set +CONFIG_QUOTA=3Dy +CONFIG_AUTOFS4_FS=3Dm +CONFIG_PROC_KCORE=3Dy +CONFIG_TMPFS=3Dy +CONFIG_NLS=3Dy +CONFIG_CRYPTO=3Dy +CONFIG_CRYPTO_CRC32C=3Dy +CONFIG_CRYPTO_JITTERENTROPY=3Dy +CONFIG_CRC16=3Dy +CONFIG_PRINTK_TIME=3Dy +CONFIG_PRINTK_CALLER=3Dy +CONFIG_DYNAMIC_DEBUG=3Dy +CONFIG_DEBUG_INFO=3Dy +CONFIG_FRAME_WARN=3D1024 +CONFIG_READABLE_ASM=3Dy +CONFIG_DEBUG_FS=3Dy +CONFIG_UBSAN=3Dy +CONFIG_PAGE_EXTENSION=3Dy +CONFIG_DEBUG_OBJECTS=3Dy +CONFIG_DEBUG_OBJECTS_FREE=3Dy +CONFIG_DEBUG_OBJECTS_TIMERS=3Dy +CONFIG_DEBUG_OBJECTS_WORK=3Dy +CONFIG_PROVE_LOCKING=3Dy +CONFIG_ENABLE_DEFAULT_TRACERS=3Dy --=20 2.34.1 From nobody Tue Jun 23 03:06:51 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 8F652C433F5 for ; Fri, 11 Mar 2022 16:28:06 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1350412AbiCKQ1t (ORCPT ); Fri, 11 Mar 2022 11:27:49 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:40838 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350387AbiCKQ0r (ORCPT ); Fri, 11 Mar 2022 11:26:47 -0500 Received: from smtp2.axis.com (smtp2.axis.com [195.60.68.18]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 2F2D41D21E2; Fri, 11 Mar 2022 08:25:10 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015914; x=1678551914; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=PfFSPTWiAxOHXxpoOxuYzI006DPifNqh9ag3eFnVu00=; b=CHxJLstLXJGaORnyxhiS9rQomGdmNsjUuQkEHZs2haPvt+X1pKcyCAAv Ptpld09X9mK14P/L/y14OqPx+B/mowuKjYg7FSJCBB79TG1lK8TTaCeZZ bTl54vy4R7KZnsldROaHpUgAtFvsc1x9hu/fEU1sDkqdTEn+ifGb45rCw Ve7wNH4VXPajAZDL5OPydSVYrPdB+KwA3jsxW4iqYyHzhxndcNaBi+7CL S967NLsmsgeG4wlp41MWBHpNMn3OLjDvsgkIA2/d6uXpYy4Lre+h3pe9U c0zyxc/6h7EH0ZLn4RuXrTDUopn2L4rfEJKtPqHH7fVbTxdKqi6r8/pNi w==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 05/10] roadtest: add build files Date: Fri, 11 Mar 2022 17:24:40 +0100 Message-ID: <20220311162445.346685-6-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Content-Type: text/plain; charset="utf-8" Add a Makefile and other miscellaneous build-related files for the roadtest framework. To make it easier to run the tests on systems which do not have the required libraries or Python version, a Dockerfile is included and the Makefile has built-in support for running the tests in a Docker container. Targets for code formatting and static checking of the Python code are included. Signed-off-by: Vincent Whitchurch --- tools/testing/roadtest/.gitignore | 2 + tools/testing/roadtest/Dockerfile | 25 ++++++++ tools/testing/roadtest/Makefile | 84 +++++++++++++++++++++++++ tools/testing/roadtest/pyproject.toml | 10 +++ tools/testing/roadtest/requirements.txt | 4 ++ tools/testing/roadtest/src/.gitignore | 1 + 6 files changed, 126 insertions(+) create mode 100644 tools/testing/roadtest/.gitignore create mode 100644 tools/testing/roadtest/Dockerfile create mode 100644 tools/testing/roadtest/Makefile create mode 100644 tools/testing/roadtest/pyproject.toml create mode 100644 tools/testing/roadtest/requirements.txt create mode 100644 tools/testing/roadtest/src/.gitignore diff --git a/tools/testing/roadtest/.gitignore b/tools/testing/roadtest/.gi= tignore new file mode 100644 index 000000000000..0cbd00343694 --- /dev/null +++ b/tools/testing/roadtest/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +.py[cod] diff --git a/tools/testing/roadtest/Dockerfile b/tools/testing/roadtest/Doc= kerfile new file mode 100644 index 000000000000..f2982179c327 --- /dev/null +++ b/tools/testing/roadtest/Dockerfile @@ -0,0 +1,25 @@ +FROM debian:bullseye + +# Kernel build +RUN apt-get update && apt-get -y install \ + bc \ + build-essential \ + flex \ + bison \ + rsync \ + kmod + +# Running roadtests +RUN apt-get update && apt-get -y install \ + python3.9 \ + libpython3.9-dev \ + python3 \ + device-tree-compiler + +# Development and debugging +RUN apt-get update && apt-get -y install \ + uml-utilities \ + telnetd \ + python3-pip +COPY requirements.txt /tmp/ +RUN pip install --requirement /tmp/requirements.txt diff --git a/tools/testing/roadtest/Makefile b/tools/testing/roadtest/Makef= ile new file mode 100644 index 000000000000..525b26581142 --- /dev/null +++ b/tools/testing/roadtest/Makefile @@ -0,0 +1,84 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +.PHONY: all build-kernel test clean check fmt docker-run + +all: + +KSOURCE :=3D ${PWD} +ROADTEST_DIR =3D ${CURDIR} +ROADTEST_BUILD_DIR :=3D ${KSOURCE}/.roadtest +KHEADERS :=3D ${ROADTEST_BUILD_DIR}/usr +KMODULES :=3D ${ROADTEST_BUILD_DIR}/modules + +ifeq (${KSOURCE},${ROADTEST_DIR}) +# Make make from the standard roadtest directory work without having to set +# additional variables. +KSOURCE=3D$(ROADTEST_DIR:/tools/testing/roadtest=3D) +endif + +CFLAGS +=3D -g -D_GNU_SOURCE=3D1 -Wall -Werror -std=3Dgnu99 \ + -I${KSOURCE}/tools/include/ \ + -I${KHEADERS}/include/ \ + -I${ROADTEST_DIR}/src/libvhost-user/ \ + $(shell python3-config --embed --includes) -O2 + +${ROADTEST_BUILD_DIR}/roadtest-backend: ${ROADTEST_BUILD_DIR}/backend.o ${= ROADTEST_BUILD_DIR}/libvhost-user.o + $(CC) -o $@ $^ $(shell python3-config --embed --libs) + # For the benefit of clangd + echo ${CFLAGS} | tr " " "\n" > ${ROADTEST_DIR}/src/compile_flags.txt + +${ROADTEST_BUILD_DIR}/backend.o: src/backend.c + $(CC) -c -o $@ $(CFLAGS) $< + +${ROADTEST_BUILD_DIR}/libvhost-user.o: src/libvhost-user/libvhost-user.c + $(CC) -c -o $@ $(CFLAGS) $< + +clean: + rm -rf ${ROADTEST_BUILD_DIR} .docker_built + +ifeq ($(DOCKER),1) +.docker_built: Dockerfile requirements.txt + docker build --network=3Dhost -t roadtest ${ROADTEST_DIR} + touch $@ + +# --network=3Dhost allows UML's con=3Dport:... to work seamlessly +docker-run: .docker_built + mkdir -p ${ROADTEST_BUILD_DIR}/umltmp + docker run --network=3Dhost ${DOCKEROPTS} --user $(shell id -u ${USER}):$= (shell id -g ${USER}) --interactive --tty --rm -v ${KSOURCE}:${KSOURCE} -w = ${KSOURCE} --env TMPDIR=3D${ROADTEST_BUILD_DIR}/umltmp roadtest sh -c '${MA= KE} -C ${ROADTEST_DIR} -${MAKEFLAGS} ${MAKECMDGOALS} DOCKER=3D0' + +all test build-kernel check fmt: docker-run + @: +else +all: test + +ifneq ($(KBUILD),0) +# Calling make on the kernel is slow even if there is nothing to be rebuil= t. +# Allow the user to avoid it with KBUILD=3D0 +${ROADTEST_BUILD_DIR}/backend.o: build-kernel +${ROADTEST_BUILD_DIR}/libvhost-user.o: build-kernel +test: build-kernel +endif + +build-kernel: + mkdir -p ${ROADTEST_BUILD_DIR} + find ${ROADTEST_DIR}/roadtest/tests/ -type f -name config | xargs cat > $= {ROADTEST_BUILD_DIR}/.config + ${MAKE} -C ${KSOURCE} ARCH=3Dum O=3D${ROADTEST_BUILD_DIR} olddefconfig + ${MAKE} -C ${KSOURCE} ARCH=3Dum O=3D${ROADTEST_BUILD_DIR} + ${MAKE} -C ${KSOURCE} ARCH=3Dum O=3D${ROADTEST_BUILD_DIR} INSTALL_HDR_PAT= H=3D${KHEADERS} headers_install + ${MAKE} -C ${KSOURCE} ARCH=3Dum O=3D${ROADTEST_BUILD_DIR} INSTALL_MOD_PAT= H=3D${KMODULES} modules_install + +test: ${ROADTEST_BUILD_DIR}/roadtest-backend + python3 -m roadtest.cmd.main --ksrc-dir ${KSOURCE} --build-dir ${ROADTEST= _BUILD_DIR} --work-dir ${ROADTEST_BUILD_DIR}/roadtest-work/ ${OPTS} + +check: + mypy --no-error-summary roadtest + pyflakes roadtest + black --check roadtest + isort --profile black --check roadtest + +fmt: + black roadtest + isort --profile black roadtest + +endif diff --git a/tools/testing/roadtest/pyproject.toml b/tools/testing/roadtest= /pyproject.toml new file mode 100644 index 000000000000..6b8b05eb3cad --- /dev/null +++ b/tools/testing/roadtest/pyproject.toml @@ -0,0 +1,10 @@ +[tool.isort] +profile =3D "black" + +[tool.mypy] +disallow_untyped_defs =3D true +check_untyped_defs =3D true +no_implicit_optional =3D true +warn_return_any =3D true +warn_unused_ignores =3D true +show_error_codes =3D true diff --git a/tools/testing/roadtest/requirements.txt b/tools/testing/roadte= st/requirements.txt new file mode 100644 index 000000000000..e1ac403d826e --- /dev/null +++ b/tools/testing/roadtest/requirements.txt @@ -0,0 +1,4 @@ +black=3D=3D22.1.0 +isort=3D=3D5.10.1 +mypy=3D=3D0.931 +pyflakes=3D=3D2.4.0 diff --git a/tools/testing/roadtest/src/.gitignore b/tools/testing/roadtest= /src/.gitignore new file mode 100644 index 000000000000..895dab3fe4be --- /dev/null +++ b/tools/testing/roadtest/src/.gitignore @@ -0,0 +1 @@ +compile_flags.txt --=20 2.34.1 From nobody Tue Jun 23 03:06:51 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 739D8C433EF for ; Fri, 11 Mar 2022 16:25:53 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S235632AbiCKQ0w (ORCPT ); Fri, 11 Mar 2022 11:26:52 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:38330 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350344AbiCKQ0g (ORCPT ); Fri, 11 Mar 2022 11:26:36 -0500 Received: from smtp2.axis.com (smtp2.axis.com [195.60.68.18]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 7135B1B8BCE; Fri, 11 Mar 2022 08:25:00 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015902; x=1678551902; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=kuBlWK4REIfqzuNarkm/sea0x7/j6Le+nchS0vdPNlo=; b=V/Vkdgqc9tB6Qze27qevEUFMrn0STCFwBRAVOvqjxCEJUmlfnOcZl88y 659/I29s84i89BHhYFAfLc6M7rkZ/HkrRDG5bOW0A0c/2zmx8GEgT4oqf xAGC1v5qAhPtbrIn3fN1DHS8V4om8dqA2ZSGLBxf5wwYc9NKAS8npYYKR PO5TVlP+9q/nzSDplMMS8gHUF5JN8FJu+ZCECTaRoRVb8q0iCkM4dy2YP XRoNuz5NRQgQKxXr/g1WaUCn7YrY7azDddY3VzNKEuCTgKxtAKnziJhvz Ok8Z0EWRm6Ko5dyRtonpQ0QUhupBOQgU1LnwMAV1MBIk90EEK2XardCse Q==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 06/10] roadtest: add documentation Date: Fri, 11 Mar 2022 17:24:41 +0100 Message-ID: <20220311162445.346685-7-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Content-Type: text/plain; charset="utf-8" Add documentation for the roadtest device driver testing framework. This includes a "how to write your first test" tutorial. Signed-off-by: Vincent Whitchurch --- Documentation/dev-tools/index.rst | 1 + Documentation/dev-tools/roadtest.rst | 669 +++++++++++++++++++++++++++ 2 files changed, 670 insertions(+) create mode 100644 Documentation/dev-tools/roadtest.rst diff --git a/Documentation/dev-tools/index.rst b/Documentation/dev-tools/in= dex.rst index 4621eac290f4..44fea7c50dad 100644 --- a/Documentation/dev-tools/index.rst +++ b/Documentation/dev-tools/index.rst @@ -33,6 +33,7 @@ Documentation/dev-tools/testing-overview.rst kselftest kunit/index ktap + roadtest =20 =20 .. only:: subproject and html diff --git a/Documentation/dev-tools/roadtest.rst b/Documentation/dev-tools= /roadtest.rst new file mode 100644 index 000000000000..114bf822e376 --- /dev/null +++ b/Documentation/dev-tools/roadtest.rst @@ -0,0 +1,669 @@ +=3D=3D=3D=3D=3D=3D=3D=3D +Roadtest +=3D=3D=3D=3D=3D=3D=3D=3D + +Roadtest is a device-driver testing framework. It tests drivers under User +Mode Linux using models of the hardware. The tests cases and hardware mod= els +are written in Python, the former using the built-in unittest framework. + +Roadtest is meant to be used for relatively simple drivers, such as the on= es +part of the IIO, regulator or RTC subsystems. + +Drivers are tested via their userspace interfaces and interact with hardwa= re +models which allow tests to inject values into registers and assert that +drivers control the hardware in the right way and react as expected to sti= muli. + +Installing the requirements +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D + +Addition to the normal requirements for building kernels, *running* roadte= st +requires Python 3.9 or later, including the development libraries: + +.. code-block:: shell + + apt-get -y install python3.9 libpython3.9-dev device-tree-compiler + +There is also support for running the tests in a Docker container without +having to install any packages. + +Running roadtest +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +To run the tests, run the following command from the base of a kernel sour= ce +tree: + +.. code-block:: shell + + $ make -C tools/testing/roadtest + +Or, if you prefer to use the Docker container: + +.. code-block:: shell + + $ make -C tools/testing/roadtest DOCKER=3D1 + +Either of these commands will build a kernel and run all roadtests. + +.. note:: + + Roadtest builds the kernel out-of-tree. The kernel build system may ins= truct + you to clean your tree if you have previously performed an in-tree build= . You + can pass the usual ``-jNN`` options to parallelize the build. The tests + themselves are currently always run sequentially. + +Writing roadtests +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + +Tutorial: Writing your first roadtest +------------------------------------- + +You may find it simplest to have a look at the existing tests and base you= r new +tests on them, but if you prefer, this section provides a tutorial which w= ill +guide you to write a new basic test from scratch. + +Even if you're not too keen on following the tutorial hands-on, you're +encouraged to skim through it since there are useful debugging tips and no= tes +on roadtest's internals which could be useful to know before diving in and +writing tests. + +A quick note on the terminology before we begin: we'll refer to the framew= ork +itself as "roadtest" or just "the framework", and we'll call a driver test +which uses this framework a "roadtest" or just a "test". + +Goal for the test +~~~~~~~~~~~~~~~~~ + +In this tutorial, we'll add a basic test for one of the features of the +VCNL4000 light sensor driver which is a part of the IIO subsystem +(``drivers/iio/light/vcnl4000.c``). + +This driver supports a bunch of related proximity and ambient light sensor +chips which communicate using the I2C protocol; we'll be testing the VCNL4= 000 +variant. The datasheet for the chip is, at the time of writing, available +`here `_. + +The test will check that the driver correctly reads and reports the illumi= nance +values from the hardware to userspace via the IIO framework. + +Test file placement +~~~~~~~~~~~~~~~~~~~ + +Roadtests are placed under ``tools/testing/roadtest/roadtest/tests``. (In= case +you're wondering, the second ``roadtest`` is to create a Python package, so +that imports of ``roadtest`` work without having to mess with module search +paths.) + +Tests are organized by subsystem. Normally we'd put our IIO light sensor = tests +under ``iio/light/`` (below the ``tests`` directory), but since there is +already a VCNL4000 test there, we'll create a new subsystem directory call= ed +``tutorial`` and put our test there in a new file called ``test_tutorial.p= y``. + +We'll also need to create an empty ``__init__.py`` in that directory to al= low +Python to recognize it as a package. + +All the commands in this tutorial should be executed from the +``tools/testing/roadtest`` directory inside the kernel source tree. (To r= educe +noise, we won't show the current working directory before the ``$`` in fut= ure +command line examples.) + +.. code-block:: shell + + tools/testing/roadtest$ mkdir -p roadtest/tests/tutorial/ + tools/testing/roadtest$ touch roadtest/tests/tutorial/__init__.py + +Building the module +~~~~~~~~~~~~~~~~~~~ + +First, we'll need to ensure that our driver is built. To do that, we'll a= dd +the appropriate config option to built our driver as a module. The lines +should be written to a new file called ``config`` in the ``tutorial`` +directory. Roadtest will gather all ``config`` files placed anywhere under +``tests`` and build a kernel with the combined config. + +.. code-block:: shell + + $ echo CONFIG_VCNL4000=3Dm >> roadtest/tests/tutorial/config + +.. note:: + + This driver will actually be built even if you don't add this config, si= nce + it's already present in the ``roadtest/tests/iio/light/config`` used by = the + existing VCNL4000 test. Roadtest uses a single build for all tests. + +Loading the module from the test +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We've set up our module to be built, so we can now start working on the te= st +case iself. We'll start with the following few lines of code. Tests are +written Python's built-in `unittest +`_ module. This tutorial= will +assume familiariy with that framework; see the Python documentation for mo= re +information. + +Test classes should subclass ``roadtest.core.suite.UMLTestCase`` instead of +``unittest.TestCase``. This informs the roadtest core code that the test +should be run inside UML. + +.. note:: + + There are several "real" unit tests for the framework itself; these subc= lass + ``unittest.TestCase`` directly and are run on the host system. You'll s= ee + these run in the beginning when you run roadtest. + +All this test currently does is insert our driver's module, do nothing, and +then remove our driver's kernel module. (The ``roadtest.core.modules.Modu= le`` +class implements a ``ContextManager`` which automatically cleans up using = the +``with`` statement.) + +.. code-block:: python + + from roadtest.core.suite import UMLTestCase + from roadtest.core.modules import Module + + class TestTutorial(UMLTestCase): + def test_illuminance(self) -> None: + with Module("vcnl4000"): + pass + +You can now build the kernel and run roadtest with: + +.. code-block:: shell + + $ make + +.. note:: + + Make sure you have all the dependencies described at the beginning of the + document installed. You can also use a Docker container, append ``DOCKE= R=3D1`` + to all the ``make`` commands in this tutorial if you want to do that. + +You should see your new test run and pass in the output of the above comma= nd: + +.. code-block:: + + ... + test_illuminance (tests.tutorial.test_tutorial.TestTutorial) ... ok + ... + +Shortening feedback loops +~~~~~~~~~~~~~~~~~~~~~~~~~ + +While just running ``make`` runs your new test, it also runs all the *othe= r* +tests too, and what's more, it calls in to the kernel build system every t= ime, +and that can be relatively slow even if there's nothing to be rebuilt. + +When you're only working on writing tests, and not modifying the driver or= the +kernel source, you can avoid calling into Kbuild by passing ``KBUILD=3D0``= to the +``make`` invocation. For example: + +.. code-block:: shell + + $ make KBUILD=3D0 + +To only run specific tests, you can use the ``--filter`` option to roadtes= t's +main script (implemented in ``roadtest.cmd.main``) which takes a wildcard +pattern. Only tests whoses names match the pattern are run. + +Options to the main script are passed via the ``OPTS`` variable. So the +following would both skip the kernel build and only run your test: + +.. code-block:: shell + + $ make KBUILD=3D0 OPTS=3D"--filter tutorial" + +.. tip:: + + Roadtest builds the kernel inside a directory called ``.roadtest`` in yo= ur + kernel source tree. Logs from UML are saved as + ``.roadtest/roadtest-work/uml.txt`` and logs from roadtest's backend (mo= re on + that later) are at ``.roadtest/roadtest-work/backend.txt``. It's someti= mes + useful to keep a terminal open running ``tail -f`` on these files while + developing roadtests. + +Adding a device +~~~~~~~~~~~~~~~ + +Our basic test only loads and unloads the module, so the next step is to +actually get our driver to probe and bind to a device. On many systems, +devices are instantiated based on the hardware descriptions in devicetree,= and +this is the case on roadtest's UML-based system too. See +:ref:`Documentation/driver-api/driver-model/binding.rst ` and +:ref:`Documentation/devicetree/usage-model.rst ` for more +information. + +When working on real harwdare, the hardware design specifies at what addre= ss +and on which I2C bus the hardware sensor chip is connected. Roadtest prov= ides +a virtual I2C bus and the test can chose to place devices at any valid add= ress +on this bus. + +In this tutorial, we'll use a hard coded device address of ``0x42`` and se= t the +``run_separately`` flag on the test, asking roadtest to run our test in a +separate UML instance so that we know that no other test has tried to put a +device at that I2C address. + +.. note:: + + Normally, roadtests use what the framework refers to as *relocatable + devicetree fragments* (unrelated to the fragments used in devicetree + overlays). These do not use fixed addreses for specific devices, but in= stead + allow the framework to freely assign addresses. This allows several + different, independent tests can be run using one devicetree and one UML + instance (to save on startup time costs), without having to coordinate + selection of device addesses. + + When writing "real" roadtests (after you're done with this tutorial), yo= u too + should use relocatable fragments. See the existing tests for examples. + +The framework's devicetree module (``roadtest.core.devicetree``) includes a +base tree that provides an I2C controller node (appropriately named ``i2c`= `) +for the virtual I2C, so we will add our new device under that node. + +Unlike on a default Linux system, just adding the node to the devicetree w= on't +get our I2C driver to automatically bind to the driver when we load the mo= dule. +This is because roadtest's ``init.sh`` (a script which runs inside UML aft= er +the kernel boots up) turns off automatic probing on the I2C bus, in order = to +give the test cases full control of when things get probed. + +So we'll have ask the ``test_illuminance()`` method to get the ``vcnl4000`` +driver (that's the name of the I2C driver which the module registers, and +that's not necessarily the same as the name of the module) to explicitly b= ind +to our chosen ``0x42`` I2C device using some of the helper classes in the +framework: + +.. code-block:: python + + from roadtest.core.devicetree import DtFragment + from roadtest.core.devices import I2CDriver + + class TestTutorial(UMLTestCase): + run_separately =3D True + dts =3D DtFragment( + src=3D""" + &i2c { + light-sensor@42 { + compatible =3D "vishay,vcnl4000"; + reg =3D <0x42>; + }; + }; + """, + ) + + def test_illuminance(self) -> None: + with ( + Module("vcnl4000"), + I2CDriver("vcnl4000").bind(0x42) as dev, + ): + pass + +You can run this test using the same ``make`` command you used previously. +This time, rather than an "ok", you should see roadtest complain about an = error +during your test: + +.. code-block:: + + =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D + ERROR: test_illuminance (tests.tutorial.test_tutorial.TestTutorial) + ---------------------------------------------------------------------- + Backend log: + Traceback (most recent call last): + File ".../roadtest/backend/i2c.py", line 35, in write + raise Exception("No I2C model loaded") + Exception: No I2C model loaded + Traceback (most recent call last): + File ".../roadtest/backend/i2c.py", line 29, in read + raise Exception("No I2C model loaded") + Exception: No I2C model loaded + + UML log: + [ 1220.410000][ T19] vcnl4000: probe of 0-0042 failed with error -5 + + Traceback (most recent call last): + File ".../roadtest/tests/tutorial/test_tutorial.py", line 21, in test_= illuminance + with ( + File "/usr/lib/python3.9/contextlib.py", line 119, in __enter__ + return next(self.gen) + File ".../roadtest/core/devices.py", line 32, in bind + f.write(dev.id.encode()) + OSError: [Errno 5] Input/output error + +To understand and fix this error, we'll have to learn a bit about how road= test +works under the hood. + +Adding a hardware model +~~~~~~~~~~~~~~~~~~~~~~~ + +Roadtest's *backend* is what allows the hardware to modelled for the sake = of +driver testing. The backend runs outside of UML and communication between= the +drivers and the models goes via ``virtio-uml``, a shared-memory based +communication protocol. At its lowest level, the backend is written in C = and +implements virtio devices for ``virtio-i2c`` and ``virtio-gpio``, both of = which +have respective virtio drivers which run inside UML and provide the virtua= l I2C +bus (and GPIO controller) whose nodes are available in the devicetree. + +The C backend embeds a Python interpreter which runs a Python module which +implements the I2C bus model. It's that Python module which is complainin= g now +that it does not have any I2C device model to handle the I2C transactions = that +it received from UML. This is quite understandable since we haven't +implemented one yet! + +.. note:: + + In the error message above, you'll also notice an error ``printk()`` fro= m the + driver (as part of the *UML log*, which includes kernel console messages= ), as + well as the exception stacktrace from the test case itself. The ``-EIO`` + seen inside UML is a result of the roadtest backend failing the I2C + transaction due to the exception. + +Models are placed in the same source file as the test cases. The model and +the test cases will however run in two different Python interpreters on two +different systems (the test case inside UML, and the model inside the back= end +on your host). + +For I2C, the interface our model needs to implement is specified by the +Abstract Base Class ``roadtest.backend.i2c.I2CModel`` (which can be found, +following Python's standard naming conventions, in the file +``roadtest/backend/i2c.py``). You can see that it expects the model to +implement ``read()`` and ``write()`` functions which transmit and receive = the +raw bytes of the I2C transaction. + +Our VCNL4000 device uses the SMBus protocol which is a subset of the I2C +protocol, so we can use a higher-level class to base our implementation of= f, +``roadtest.backend.i2c.SMBusModel``. This one takes care of doing segment= ation +of the I2C requests, and expects subclasses to implement ``reg_read()`` and +``reg_write()`` methods which will handle the register access for the devi= ce. + +For our initial model, we'll just going to just make our ``reg_read()`` and +``reg_write()`` methods read and store the register values in a dictionary. +We'll need some initial values for the registers, and for these we use the +values which are specified in the VCNL4000's datasheet. We won't bother w= ith +creating constants for the register addresses and we'll just specify them = in +hex: + +.. code-block:: python + + from typing import Any + from roadtest.backend.i2c import SMBusModel + + class VCNL4000(SMBusModel): + def __init__(self, **kwargs: Any) -> None: + super().__init__(regbytes=3D1, **kwargs) + self.regs =3D { + 0x80: 0b_1000_0000, + 0x81: 0x11, + 0x82: 0x00, + 0x83: 0x00, + 0x84: 0x00, + 0x85: 0x00, + 0x86: 0x00, + 0x87: 0x00, + 0x88: 0x00, + 0x89: 0x00, + } + + def reg_read(self, addr: int) -> int: + val =3D self.regs[addr] + return val + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + self.regs[addr] =3D val + +Then we need to modify the test function to ask the backend to load this m= odel: + +.. code-block:: python + :emphasize-lines: 1,6 + + from roadtest.core.hardware import Hardware + + def test_illuminance(self) -> None: + with ( + Module("vcnl4000"), + Hardware("i2c").load_model(VCNL4000), + I2CDriver("vcnl4000").bind(0x42), + ): + pass + +Now run the test again. You should see the test pass, meaning that the dr= iver +successfully talked to and recognized your hardware model. (You can look = at +the UML and backend logs mentioned earlier to confirm this.) + +.. tip:: + + You can add arbitrary command line arguments to UML using the + ``--uml-append`` option. For example, while developing tests for I2C + drivers, it could be helpful to turn on the appropriate trace events and + arrange for them to be printed to the console (which you can then access= via + the previously mentioned ``uml.txt``.): + + .. code-block:: + + OPTS=3D"--filter tutorial --uml-append tp_printk trace_event=3Di2c:*" + +Exploring the target +~~~~~~~~~~~~~~~~~~~~ + +Now that we've gotten the driver to probe to our new device, we want to ge= t the +test to read the illuminance value from the driver. However, which file s= hould +the test read the value from? IIO exposes the illuminance value in a sysfs +file, but where do we find this file? + +If you have real hardware with a VCNL4000 chip and already running the vcn= l4000 +driver, or are already very familiar with the IIO framework, you likely al= ready +know what sysfs files to read, but in our case, we can open up a shell on = UML +to manually explore the system and find the relevant sysfs files before +implementing the rest of the test case. + +Roadtest's ``--shell`` option makes UML start a shell instead of exiting a= fter +the tests are run. However, since our test case cleans up after itself (as +it should) using context managers, neither the module nor the model would +remain loaded after the test exists, which would make manual exploration +difficult. + +To remedy this, we can combine ``--shell`` with temporary code in our test= to +_exit(2) after setting up everything: + +.. code-block:: python + :emphasize-lines: 5,7 + + def test_illuminance(self) -> None: + with ( + Module("vcnl4000"), + Hardware("i2c").load_model(VCNL4000), + I2CDriver("vcnl4000").bind(0x42) as dev, + ): + print(dev.path) + import os; os._exit(1) + +.. note:: + + The communication between the test cases and the models uses a simple te= xt + based protocol where the test cases write Python expressions to a file w= hich + the backend reads and evaluates, so it is possible to load a model using= only + shell commands, but this is undocumented. See the source code if you ne= ed to + do this. + +We'll also need to ask UML to open up a terminal emulator (``con=3Dxterm``= ) or start a telnet server +and wait for a connection (``con=3Dport:9000``). See +:ref:`Documentation/virt/uml/user_mode_linux_hotwo_v2.rst +` for more information about the required packag= es. +These options can be passed to UML using ``--uml-append``. So the final +``OPTS`` argument is something like the following (you can combine this wi= th +the tracing options): + +.. code-block:: + + OPTS=3D"--shell --uml-append con=3Dxterm" + +.. tip:: + + ``con=3Dxterm doesn``'t work in the Docker container, so use the telnet = option + if you're running roadtest inside Docker. ``screen -L //telnet localhost + 9000`` or similar can be used to connect to UML. + + When running *without* using Docker, the telnet option tends to leave UM= L's + ``port-helper`` running in the background, so you may have to ``kill(1)`= ` it + yourself after each run. + +Using the shell, you should be able to find the illuminance file under the +device's sysfs path: + +.. code-block:: + + root@(none):/sys/bus/i2c/devices/0-0042# ls -1 iio\:device0/in* + iio:device0/in_illuminance_raw + iio:device0/in_illuminance_scale + iio:device0/in_proximity_nearlevel + iio:device0/in_proximity_raw + +You can also attempt to read the ``in_illuminance_raw`` file; you should s= ee +that it fails with something like this (with the trace events enabled): + +.. code-block:: + + root@(none):/sys/bus/i2c/devices/0-0042# cat iio:device0/in_illuminance_= raw + [ 151.270000][ T34] i2c_write: i2c-0 #0 a=3D042 f=3D0000 l=3D2 [80-10] + [ 151.270000][ T34] i2c_result: i2c-0 n=3D1 ret=3D1 + ... + [ 152.030000][ T34] i2c_write: i2c-0 #0 a=3D042 f=3D0000 l=3D1 [80] + [ 152.030000][ T34] i2c_read: i2c-0 #1 a=3D042 f=3D0001 l=3D1 + [ 152.030000][ T34] i2c_reply: i2c-0 #1 a=3D042 f=3D0001 l=3D1 [10] + [ 152.030000][ T34] i2c_result: i2c-0 n=3D2 ret=3D2 + [ 152.070000][ T34] vcnl4000 0-0042: vcnl4000_measure() failed, data = not ready + +Controlling register values +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Our next challenge is to get the ``in_illuminance_raw`` file to be read +successfully. From the I2C trace events above, or from looking at the +``backend.txt`` (below), we can see that the driver repeatedly reads a +particular register. + +.. code-block:: + + INFO - roadtest.core.control: START + DEBUG - roadtest.core.control: backend.i2c.load_model(*('roadtest.tests.= tutorial.test_tutorial', 'VCNL4000'), **{}) + DEBUG - roadtest.backend.i2c: SMBus read addr=3D0x81 val=3D0x11 + DEBUG - roadtest.backend.i2c: SMBus write addr=3D0x80 val=3D0x10 + DEBUG - roadtest.backend.i2c: SMBus read addr=3D0x80 val=3D0x10 + DEBUG - roadtest.backend.i2c: SMBus read addr=3D0x80 val=3D0x10 + ... + +To understand this register, we need to take a look at the chip's datashee= t and +compare it with the driver code. By doing so, we can see the driver is wa= iting +for the hardware to signal that the data is ready by polling for a particu= lar +bit to be set. + +One simple way to set the data ready bit, which we'll use for the purpose = of +this tutorial, is to simply ensure that the model always returns reads to = the +0x80 register with that bit set. + +.. note:: + + This method wouldn't allow a test to be written to test the timeout hand= ling, + but we won't bother with that in this tutorial. You can explore the exi= sing + roadtests for alternative solutions, such as setting the data ready bit + whenever the test injects new data and clearing it when the driver reads= the + data. + +.. code-block:: python + :emphasize-lines: 4,5 + + def reg_read(self, addr: int) -> int: + val =3D self.regs[addr] + + if addr =3D=3D 0x80: + val |=3D 1 << 6 + + return val + +This should get the bit set and make the read succeed (you can check this = using +the shell), but we'd also like to return different values from the data +registers rather the reset values we hardcoded in ``__init__``. One way t= o do +this is to have the test inject the values into the ALS result registers by +having it call the ``reg_write()`` method of the model. It can do this vi= a the +``Hardware`` object. + +.. note:: + + The test can call methods on the model but it can't receive return values + from these methods, nor can it set attributes on the model. The model a= nd + the test run on different systems and communication between them is + asynchronous. + +We'll combine this with a read of the sysfs file we identified and throw i= n an +assertion to check that the value which the driver reports to userspace via +that file matches the value which we inject into the hardware's result +registers: + +.. code-block:: python + :emphasize-lines: 6,8,9-13 + + from roadtest.core.sysfs import read_int + + def test_illuminance(self) -> None: + with ( + Module("vcnl4000"), + Hardware("i2c").load_model(VCNL4000) as hw, + I2CDriver("vcnl4000").bind(0x42) as dev, + ): + hw.reg_write(0x85, 0x12) + hw.reg_write(0x86, 0x34) + self.assertEqual( + read_int(dev.path / "iio:device0/in_illuminance_raw", 0x12= 34) + ) + +And that's it for this tutorial. We've written a simple end-to-end test f= or +one aspect of this driver with the help of a minimal model of the hardware. + +Verifying drivers' interactions with the hardware +------------------------------------------------- + +The tutorial covered injection of values into hardware registers and how to +check that the driver interprets the value exposed by the hardware correct= ly, +but another important aspect of testing device drivers is to verify that t= he +driver actually *controls* the hardware in the expected way. + +For example, if you are testing a regulator driver, you want to test that +driver actually writes the correct voltage register in the hardware with t= he +correct value when the driver is asked to set a voltage using the kernel's +regulator API. + +To support this, roadtest integrates with Python's built-in `unittest.mock +`_ library. The +``update_mock()`` method on the ``Hardware`` objects results in a ``HwMock= `` (a +subclass of ``unittest.mock``'s ``MagicMock``) object which, in the case of +``SMBusModel``, provides access to a log of all register writes and their +values. + +The object can be then used to check which registers the hardware has writ= ten +with which values, and to assert that the expect actions have been taken. + +See ``roadtest/tests/regulator/test_tps62864.py`` for an example of this. + +GPIOs +----- + +The framework includes support for hardware models to trigger interrupts by +controlling GPIOs. See ``roadtest/tests/rtc/test_pcf8563.py`` for an exam= ple. + +Support has not been implemented yet for asserting that drivers control GP= IOs +correctly. See the comment in ``gpio_handle_cmdq()`` in ``src/backend.c``. + +Coding guidelines +----------------- + +Run ``make fmt`` to automatically format your Python code to follow the co= ding +style. Run ``make check`` and ensure that your code passes static checker= s and +style checks. Typing hints are mandatory. + +These two commands require that you have installed the packages listed in +``requirements.txt``, for example with something like the following patch = and +then ensuring that ``~/.local/bin`` is in your ``$PATH``. + +.. code-block:: shell + + $ pip3 install --user -r requirements.txt + +Alternatively, you can also run these commands in the Docker container (by +appending ``DOCKER=3D1`` to the ``make`` commands) which has all the corre= ct +tools installed. --=20 2.34.1 From nobody Tue Jun 23 03:06:51 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id A3C2AC433EF for ; Fri, 11 Mar 2022 16:26:21 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1350448AbiCKQ1U (ORCPT ); Fri, 11 Mar 2022 11:27:20 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:40798 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350320AbiCKQ0q (ORCPT ); Fri, 11 Mar 2022 11:26:46 -0500 Received: from smtp1.axis.com (smtp1.axis.com [195.60.68.17]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 8C1BD1C4B19; Fri, 11 Mar 2022 08:25:08 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015910; x=1678551910; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=SOwKI/JWlE04bP10q/tiaI9Kor0jtXpP5KU+ERCqxE8=; b=dUbUv7mWtf+c3LWzRTfbJkhMrEtDtCiaeRGU8CH1Yq/h6X9L3V6jZkIH y/odUFE3MUF94LzL7R86u1K9EQekBS3qdPQPQaDGHbVI9Frj5ndn53Yvm 2Vz9JbadMPime4MsqhgBaRMIV3Z4fMFD0W5ldK1xZZLqyML2A3owB35s7 wNV4nPftyCaSDUUI+mpyclSAGGCw2cO0xvabEFT3Ddl6eOC2yxGqGbNrA vGJ47m6s9yPXiV+JNsV/yi75+S/WaubAncWPAn2V2OhveumOrH78o0r5N sWTbFvWiPerWD4PWR1oUzZwhoo3kdLf6Kc9Ao0z9zaOw6CsvJ2FfetGkH Q==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 07/10] iio: light: opt3001: add roadtest Date: Fri, 11 Mar 2022 17:24:42 +0100 Message-ID: <20220311162445.346685-8-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Content-Type: text/plain; charset="utf-8" Add a regression test for the problem fixed by the following patch, which would require specific environmental conditions to be able to be reproduced and regression-tested on real hardware: iio: light: opt3001: Fixed timeout error when 0 lux https://lore.kernel.org/lkml/20210920125351.6569-1-valek@2n.cz/ No other aspects of the driver are tested. Signed-off-by: Vincent Whitchurch --- .../roadtest/roadtest/tests/iio/__init__.py | 0 .../roadtest/roadtest/tests/iio/config | 1 + .../roadtest/tests/iio/light/__init__.py | 0 .../roadtest/roadtest/tests/iio/light/config | 1 + .../roadtest/tests/iio/light/test_opt3001.py | 95 +++++++++++++++++++ 5 files changed, 97 insertions(+) create mode 100644 tools/testing/roadtest/roadtest/tests/iio/__init__.py create mode 100644 tools/testing/roadtest/roadtest/tests/iio/config create mode 100644 tools/testing/roadtest/roadtest/tests/iio/light/__init_= _.py create mode 100644 tools/testing/roadtest/roadtest/tests/iio/light/config create mode 100644 tools/testing/roadtest/roadtest/tests/iio/light/test_op= t3001.py diff --git a/tools/testing/roadtest/roadtest/tests/iio/__init__.py b/tools/= testing/roadtest/roadtest/tests/iio/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/testing/roadtest/roadtest/tests/iio/config b/tools/testi= ng/roadtest/roadtest/tests/iio/config new file mode 100644 index 000000000000..a08d9e23ce38 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/iio/config @@ -0,0 +1 @@ +CONFIG_IIO=3Dy diff --git a/tools/testing/roadtest/roadtest/tests/iio/light/__init__.py b/= tools/testing/roadtest/roadtest/tests/iio/light/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/testing/roadtest/roadtest/tests/iio/light/config b/tools= /testing/roadtest/roadtest/tests/iio/light/config new file mode 100644 index 000000000000..b9753f2d0728 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/iio/light/config @@ -0,0 +1 @@ +CONFIG_OPT3001=3Dm diff --git a/tools/testing/roadtest/roadtest/tests/iio/light/test_opt3001.p= y b/tools/testing/roadtest/roadtest/tests/iio/light/test_opt3001.py new file mode 100644 index 000000000000..abf20b8f3516 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/iio/light/test_opt3001.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +from typing import Any, Final + +from roadtest.backend.i2c import SMBusModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod, rmmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import I2CDriver, read_float + +REG_RESULT: Final =3D 0x00 +REG_CONFIGURATION: Final =3D 0x01 +REG_LOW_LIMIT: Final =3D 0x02 +REG_HIGH_LIMIT: Final =3D 0x03 +REG_MANUFACTURER_ID: Final =3D 0x7E +REG_DEVICE_ID: Final =3D 0x7F + +REG_CONFIGURATION_CRF: Final =3D 1 << 7 + + +class OPT3001(SMBusModel): + def __init__(self, **kwargs: Any) -> None: + super().__init__(regbytes=3D2, byteorder=3D"big", **kwargs) + # Reset values from datasheet + self.regs =3D { + REG_RESULT: 0x0000, + REG_CONFIGURATION: 0xC810, + REG_LOW_LIMIT: 0xC000, + REG_HIGH_LIMIT: 0xBFFF, + REG_MANUFACTURER_ID: 0x5449, + REG_DEVICE_ID: 0x3001, + } + + def reg_read(self, addr: int) -> int: + val =3D self.regs[addr] + + if addr =3D=3D REG_CONFIGURATION: + # Always indicate that the conversion is ready. This is good + # enough for our current purposes. + val |=3D REG_CONFIGURATION_CRF + + return val + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + self.regs[addr] =3D val + + +class TestOPT3001(UMLTestCase): + dts =3D DtFragment( + src=3D""" +&i2c { + light-sensor@$addr$ { + compatible =3D "ti,opt3001"; + reg =3D <0x$addr$>; + }; +}; + """, + variables=3D{ + "addr": DtVar.I2C_ADDR, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("opt3001") + + @classmethod + def tearDownClass(cls) -> None: + rmmod("opt3001") + + def setUp(self) -> None: + self.driver =3D I2CDriver("opt3001") + self.hw =3D Hardware("i2c") + self.hw.load_model(OPT3001) + + def tearDown(self) -> None: + self.hw.close() + + def test_illuminance(self) -> None: + data =3D [ + # Some values from datasheet, and 0 + (0b_0000_0000_0000_0000, 0), + (0b_0000_0000_0000_0001, 0.01), + (0b_0011_0100_0101_0110, 88.80), + (0b_0111_1000_1001_1010, 2818.56), + ] + with self.driver.bind(self.dts["addr"]) as dev: + luxfile =3D dev.path / "iio:device0/in_illuminance_input" + + for regval, lux in data: + self.hw.reg_write(REG_RESULT, regval) + self.assertEqual(read_float(luxfile), lux) --=20 2.34.1 From nobody Tue Jun 23 03:06:51 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 340F5C433FE for ; Fri, 11 Mar 2022 16:26:43 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1350474AbiCKQ1o (ORCPT ); Fri, 11 Mar 2022 11:27:44 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:37596 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350328AbiCKQ0r (ORCPT ); Fri, 11 Mar 2022 11:26:47 -0500 Received: from smtp2.axis.com (smtp2.axis.com [195.60.68.18]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 8540A1D178C; Fri, 11 Mar 2022 08:25:09 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015914; x=1678551914; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=2fctezGvjdiZGRh6wrKTd5SnqfgQb4vo1grBqwFZpAI=; b=A1elt0X77uBkJat/ZRyuc8evduF0sDRwuYlNfF2ZKcEqOrX9qen9uw3p hM0xzn11p/R8d4ebq1D6fmsuMmwATOdzFS4+ZR8YiwKuHkyfKdsgm+ky5 olslcyMBzPDWocu5ZwqSWRrbz7XEJoUe3G+FKeH9iL75DTPod3+tTj7lv wnbqbx4FqDt89RRaTtSEh+oD4cHTZzDjSHTTeovPXkY29z2TDv8zy2jCi X1B8H9wrChk+/++j5DJ0PBhPrLo+UmYgJt++ijkkQJUyGsjBYU7yWJ12T AN9CG6EQO1QEOCLO8VWDqLWuH1YRkvG5yZtHpb2Kq8zjmO4mhifr/95SC w==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 08/10] iio: light: vcnl4000: add roadtest Date: Fri, 11 Mar 2022 17:24:43 +0100 Message-ID: <20220311162445.346685-9-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Content-Type: text/plain; charset="utf-8" Add roadtests for the vcnl4000 driver, testing several of the driver's features including buffer and event handling. Since it's the first IIO roadtest testing the non-sysfs parts, some support code for using the IIO ABI is included. The different variants supported by the driver are in separate tests and models since no two variants have fully identical register interfaces. This duplicates some of the test code, but it: - Avoids the tests duplicating the same multi-variant logic as the driver, reducing the risk for both the test and the driver being wrong. - Allows each variant's test and model to be individually understood and modified looking at only one specific datasheet, making it easier to extend tests and implement new features in the driver. During development of these tests, two oddities were noticed in the driver's handling of VCNL4040, but the tests simply assume that the current driver knows what it's doing (although we may want to fix the first point later): - The driver reads an invalid/undefined register on the VCNL4040 when attempting to distinguish between that one and VCNL4200. - The driver uses a lux/step unit which differs from the datasheet (but which is specified in an application note). Signed-off-by: Vincent Whitchurch --- .../roadtest/roadtest/tests/iio/iio.py | 112 +++++++ .../roadtest/roadtest/tests/iio/light/config | 1 + .../roadtest/tests/iio/light/test_vcnl4000.py | 132 ++++++++ .../roadtest/tests/iio/light/test_vcnl4010.py | 282 ++++++++++++++++++ .../roadtest/tests/iio/light/test_vcnl4040.py | 104 +++++++ .../roadtest/tests/iio/light/test_vcnl4200.py | 96 ++++++ 6 files changed, 727 insertions(+) create mode 100644 tools/testing/roadtest/roadtest/tests/iio/iio.py create mode 100644 tools/testing/roadtest/roadtest/tests/iio/light/test_vc= nl4000.py create mode 100644 tools/testing/roadtest/roadtest/tests/iio/light/test_vc= nl4010.py create mode 100644 tools/testing/roadtest/roadtest/tests/iio/light/test_vc= nl4040.py create mode 100644 tools/testing/roadtest/roadtest/tests/iio/light/test_vc= nl4200.py diff --git a/tools/testing/roadtest/roadtest/tests/iio/iio.py b/tools/testi= ng/roadtest/roadtest/tests/iio/iio.py new file mode 100644 index 000000000000..ea57b28ea9d3 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/iio/iio.py @@ -0,0 +1,112 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import contextlib +import enum +import fcntl +import struct +from dataclasses import dataclass, field +from typing import Any + +IIO_GET_EVENT_FD_IOCTL =3D 0x80046990 +IIO_BUFFER_GET_FD_IOCTL =3D 0xC0046991 + + +class IIOChanType(enum.IntEnum): + IIO_VOLTAGE =3D 0 + IIO_CURRENT =3D 1 + IIO_POWER =3D 2 + IIO_ACCEL =3D 3 + IIO_ANGL_VEL =3D 4 + IIO_MAGN =3D 5 + IIO_LIGHT =3D 6 + IIO_INTENSITY =3D 7 + IIO_PROXIMITY =3D 8 + IIO_TEMP =3D 9 + IIO_INCLI =3D 10 + IIO_ROT =3D 11 + IIO_ANGL =3D 12 + IIO_TIMESTAMP =3D 13 + IIO_CAPACITANCE =3D 14 + IIO_ALTVOLTAGE =3D 15 + IIO_CCT =3D 16 + IIO_PRESSURE =3D 17 + IIO_HUMIDITYRELATIVE =3D 18 + IIO_ACTIVITY =3D 19 + IIO_STEPS =3D 20 + IIO_ENERGY =3D 21 + IIO_DISTANCE =3D 22 + IIO_VELOCITY =3D 23 + IIO_CONCENTRATION =3D 24 + IIO_RESISTANCE =3D 25 + IIO_PH =3D 26 + IIO_UVINDEX =3D 27 + IIO_ELECTRICALCONDUCTIVITY =3D 28 + IIO_COUNT =3D 29 + IIO_INDEX =3D 30 + IIO_GRAVITY =3D 31 + IIO_POSITIONRELATIVE =3D 32 + IIO_PHASE =3D 33 + IIO_MASSCONCENTRATION =3D 34 + + +@dataclass +class IIOEvent: + id: int + timestamp: int + type: IIOChanType =3D field(init=3DFalse) + + def __post_init__(self) -> None: + self.type =3D IIOChanType((self.id >> 32) & 0xFF) + + +class IIOEventMonitor(contextlib.AbstractContextManager): + def __init__(self, devname: str) -> None: + self.devname =3D devname + + def __enter__(self) -> "IIOEventMonitor": + self.file =3D open(self.devname, "rb") + + s =3D struct.Struct("L") + buf =3D bytearray(s.size) + fcntl.ioctl(self.file.fileno(), IIO_GET_EVENT_FD_IOCTL, buf) + eventfd =3D s.unpack(buf)[0] + self.eventf =3D open(eventfd, "rb") + + return self + + def read(self) -> IIOEvent: + s =3D struct.Struct("Qq") + buf =3D self.eventf.read(s.size) + return IIOEvent(*s.unpack(buf)) + + def __exit__(self, *_: Any) -> None: + self.eventf.close() + self.file.close() + + +class IIOBuffer(contextlib.AbstractContextManager): + def __init__(self, devname: str, bufidx: int) -> None: + self.devname =3D devname + self.bufidx =3D bufidx + + def __enter__(self) -> "IIOBuffer": + self.file =3D open(self.devname, "rb") + + s =3D struct.Struct("L") + buf =3D bytearray(s.size) + s.pack_into(buf, 0, self.bufidx) + fcntl.ioctl(self.file.fileno(), IIO_BUFFER_GET_FD_IOCTL, buf) + eventfd =3D s.unpack(buf)[0] + self.eventf =3D open(eventfd, "rb") + + return self + + def read(self, spec: str) -> tuple: + s =3D struct.Struct(spec) + buf =3D self.eventf.read(s.size) + return s.unpack(buf) + + def __exit__(self, *_: Any) -> None: + self.eventf.close() + self.file.close() diff --git a/tools/testing/roadtest/roadtest/tests/iio/light/config b/tools= /testing/roadtest/roadtest/tests/iio/light/config index b9753f2d0728..3bd4125cbb6b 100644 --- a/tools/testing/roadtest/roadtest/tests/iio/light/config +++ b/tools/testing/roadtest/roadtest/tests/iio/light/config @@ -1 +1,2 @@ CONFIG_OPT3001=3Dm +CONFIG_VCNL4000=3Dm diff --git a/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4000.= py b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4000.py new file mode 100644 index 000000000000..16a5bed18b7e --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4000.py @@ -0,0 +1,132 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import errno +import logging +from typing import Any, Final + +from roadtest.backend.i2c import SMBusModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod, rmmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import I2CDriver, read_float, read_int, read_str + +logger =3D logging.getLogger(__name__) + +REG_COMMAND: Final =3D 0x80 +REG_PRODUCT_ID_REVISION: Final =3D 0x81 +REG_IR_LED_CURRENT: Final =3D 0x83 +REG_ALS_PARAM: Final =3D 0x84 +REG_ALS_RESULT_HIGH: Final =3D 0x85 +REG_ALS_RESULT_LOW: Final =3D 0x86 +REG_PROX_RESULT_HIGH: Final =3D 0x87 +REG_PROX_RESULT_LOW: Final =3D 0x88 +REG_PROX_SIGNAL_FREQ: Final =3D 0x89 + +REG_COMMAND_ALS_DATA_RDY: Final =3D 1 << 6 +REG_COMMAND_PROX_DATA_RDY: Final =3D 1 << 5 + + +class VCNL4000(SMBusModel): + def __init__(self, **kwargs: Any) -> None: + super().__init__(regbytes=3D1, **kwargs) + self.regs =3D { + REG_COMMAND: 0b_1000_0000, + REG_PRODUCT_ID_REVISION: 0x11, + # Register "without function in current version" + 0x82: 0x00, + REG_IR_LED_CURRENT: 0x00, + REG_ALS_PARAM: 0x00, + REG_ALS_RESULT_HIGH: 0x00, + REG_ALS_RESULT_LOW: 0x00, + REG_PROX_RESULT_HIGH: 0x00, + REG_PROX_RESULT_LOW: 0x00, + REG_PROX_RESULT_LOW: 0x00, + } + + def reg_read(self, addr: int) -> int: + val =3D self.regs[addr] + + if addr in (REG_ALS_RESULT_HIGH, REG_ALS_RESULT_LOW): + self.regs[REG_COMMAND] &=3D ~REG_COMMAND_ALS_DATA_RDY + if addr in (REG_PROX_RESULT_HIGH, REG_PROX_RESULT_LOW): + self.regs[REG_COMMAND] &=3D ~REG_COMMAND_PROX_DATA_RDY + + return val + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + + if addr =3D=3D REG_COMMAND: + rw =3D 0b_0001_1000 + val =3D (self.regs[addr] & ~rw) | (val & rw) + + self.regs[addr] =3D val + + def inject(self, addr: int, val: int, mask: int =3D ~0) -> None: + old =3D self.regs[addr] & ~mask + new =3D old | (val & mask) + self.regs[addr] =3D new + + +class TestVCNL4000(UMLTestCase): + dts =3D DtFragment( + src=3D""" +&i2c { + light-sensor@$addr$ { + compatible =3D "vishay,vcnl4000"; + reg =3D <0x$addr$>; + }; +}; + """, + variables=3D{ + "addr": DtVar.I2C_ADDR, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("vcnl4000") + + @classmethod + def tearDownClass(cls) -> None: + rmmod("vcnl4000") + + def setUp(self) -> None: + self.driver =3D I2CDriver("vcnl4000") + self.hw =3D Hardware("i2c") + self.hw.load_model(VCNL4000) + + def tearDown(self) -> None: + self.hw.close() + + def test_lux(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + scale =3D read_float(dev.path / "iio:device0/in_illuminance_sc= ale") + self.assertEqual(scale, 0.25) + + data =3D [ + (0x00, 0x00), + (0x12, 0x34), + (0xFF, 0xFF), + ] + luxfile =3D dev.path / "iio:device0/in_illuminance_raw" + for high, low in data: + self.hw.inject(REG_ALS_RESULT_HIGH, high) + self.hw.inject(REG_ALS_RESULT_LOW, low) + self.hw.inject( + REG_COMMAND, + val=3DREG_COMMAND_ALS_DATA_RDY, + mask=3DREG_COMMAND_ALS_DATA_RDY, + ) + + self.assertEqual(read_int(luxfile), high << 8 | low) + + def test_lux_timeout(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + # self.hw.set_never_ready(True) + with self.assertRaises(OSError) as cm: + luxfile =3D dev.path / "iio:device0/in_illuminance_raw" + read_str(luxfile) + self.assertEqual(cm.exception.errno, errno.EIO) diff --git a/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4010.= py b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4010.py new file mode 100644 index 000000000000..929db970405f --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4010.py @@ -0,0 +1,282 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import errno +import logging +from pathlib import Path +from typing import Any, Final, Optional + +from roadtest.backend.i2c import SMBusModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod, rmmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import ( + I2CDriver, + read_float, + read_int, + read_str, + write_int, + write_str, +) +from roadtest.tests.iio import iio + +logger =3D logging.getLogger(__name__) + +REG_COMMAND: Final =3D 0x80 +REG_PRODUCT_ID_REVISION: Final =3D 0x81 +REG_PROXIMITY_RATE: Final =3D 0x82 +REG_IR_LED_CURRENT: Final =3D 0x83 +REG_ALS_PARAM: Final =3D 0x84 +REG_ALS_RESULT_HIGH: Final =3D 0x85 +REG_ALS_RESULT_LOW: Final =3D 0x86 +REG_PROX_RESULT_HIGH: Final =3D 0x87 +REG_PROX_RESULT_LOW: Final =3D 0x88 +REG_INTERRUPT_CONTROL: Final =3D 0x89 +REG_LOW_THRESHOLD_HIGH: Final =3D 0x8A +REG_LOW_THRESHOLD_LOW: Final =3D 0x8B +REG_HIGH_THRESHOLD_HIGH: Final =3D 0x8C +REG_HIGH_THRESHOLD_LOW: Final =3D 0x8D +REG_INTERRUPT_STATUS: Final =3D 0x8E + +REG_COMMAND_ALS_DATA_RDY: Final =3D 1 << 6 +REG_COMMAND_PROX_DATA_RDY: Final =3D 1 << 5 + + +class VCNL4010(SMBusModel): + def __init__(self, int: Optional[int] =3D None, **kwargs: Any) -> None: + super().__init__(regbytes=3D1, **kwargs) + self.int =3D int + self._set_int(False) + self.regs =3D { + REG_COMMAND: 0b_1000_0000, + REG_PRODUCT_ID_REVISION: 0x21, + REG_PROXIMITY_RATE: 0x00, + REG_IR_LED_CURRENT: 0x00, + REG_ALS_PARAM: 0x00, + REG_ALS_RESULT_HIGH: 0x00, + REG_ALS_RESULT_LOW: 0x00, + REG_PROX_RESULT_HIGH: 0x00, + REG_PROX_RESULT_LOW: 0x00, + REG_INTERRUPT_CONTROL: 0x00, + REG_LOW_THRESHOLD_HIGH: 0x00, + REG_LOW_THRESHOLD_LOW: 0x00, + REG_HIGH_THRESHOLD_HIGH: 0x00, + REG_HIGH_THRESHOLD_LOW: 0x00, + REG_INTERRUPT_STATUS: 0x00, + } + + def _set_int(self, active: int) -> None: + # Active-low + self.backend.gpio.set(self.int, not active) + + def _update_irq(self) -> None: + selftimed_en =3D self.regs[REG_COMMAND] & (1 << 0) + prox_en =3D self.regs[REG_COMMAND] & (1 << 1) + prox_data_rdy =3D self.regs[REG_COMMAND] & REG_COMMAND_PROX_DATA_R= DY + int_prox_ready_en =3D self.regs[REG_INTERRUPT_CONTROL] & (1 << 3) + + logger.debug( + f"{selftimed_en=3D:x} {prox_en=3D:x} {prox_data_rdy=3D:x} {int= _prox_ready_en=3D:x}" + ) + + if selftimed_en and prox_en and prox_data_rdy and int_prox_ready_e= n: + self.regs[REG_INTERRUPT_STATUS] |=3D 1 << 3 + + low_threshold =3D ( + self.regs[REG_LOW_THRESHOLD_HIGH] << 8 | self.regs[REG_LOW_THR= ESHOLD_LOW] + ) + high_threshold =3D ( + self.regs[REG_HIGH_THRESHOLD_HIGH] << 8 | self.regs[REG_HIGH_T= HRESHOLD_LOW] + ) + proximity =3D ( + self.regs[REG_PROX_RESULT_HIGH] << 8 | self.regs[REG_PROX_RESU= LT_LOW] + ) + int_thres_en =3D self.regs[REG_INTERRUPT_CONTROL] & (1 << 1) + + logger.debug( + f"{low_threshold=3D:x} {high_threshold=3D:x} {proximity=3D:x} = {int_thres_en=3D:x}" + ) + + if int_thres_en: + if proximity < low_threshold: + logger.debug("LOW") + self.regs[REG_INTERRUPT_STATUS] |=3D 1 << 1 + if proximity > high_threshold: + logger.debug("HIGH") + self.regs[REG_INTERRUPT_STATUS] |=3D 1 << 0 + + self._set_int(self.regs[REG_INTERRUPT_STATUS]) + + def reg_read(self, addr: int) -> int: + val =3D self.regs[addr] + + if addr in (REG_ALS_RESULT_HIGH, REG_ALS_RESULT_LOW): + self.regs[REG_COMMAND] &=3D ~REG_COMMAND_ALS_DATA_RDY + if addr in (REG_PROX_RESULT_HIGH, REG_PROX_RESULT_LOW): + self.regs[REG_COMMAND] &=3D ~REG_COMMAND_PROX_DATA_RDY + + return val + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + + if addr =3D=3D REG_COMMAND: + rw =3D 0b_0001_1111 + val =3D (self.regs[addr] & ~rw) | (val & rw) + elif addr =3D=3D REG_INTERRUPT_STATUS: + val =3D self.regs[addr] & ~(val & 0xF) + + self.regs[addr] =3D val + self._update_irq() + + def inject(self, addr: int, val: int, mask: int =3D ~0) -> None: + old =3D self.regs[addr] & ~mask + new =3D old | (val & mask) + self.regs[addr] =3D new + self._update_irq() + + def set_bit(self, addr: int, val: int) -> None: + self.inject(addr, val, val) + + +class TestVCNL4010(UMLTestCase): + dts =3D DtFragment( + src=3D""" +#include + +&i2c { + light-sensor@$addr$ { + compatible =3D "vishay,vcnl4020"; + reg =3D <0x$addr$>; + interrupt-parent =3D <&gpio>; + interrupts =3D <$gpio$ IRQ_TYPE_EDGE_FALLING>; + }; +}; + """, + variables=3D{ + "addr": DtVar.I2C_ADDR, + "gpio": DtVar.GPIO_PIN, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("vcnl4000") + + @classmethod + def tearDownClass(cls) -> None: + rmmod("vcnl4000") + + def setUp(self) -> None: + self.driver =3D I2CDriver("vcnl4000") + self.hw =3D Hardware("i2c") + self.hw.load_model(VCNL4010, int=3Dself.dts["gpio"]) + + def tearDown(self) -> None: + self.hw.close() + + def test_lux(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + + scale =3D read_float(dev.path / "iio:device0/in_illuminance_sc= ale") + self.assertEqual(scale, 0.25) + + data =3D [ + (0x00, 0x00), + (0x12, 0x34), + (0xFF, 0xFF), + ] + luxfile =3D dev.path / "iio:device0/in_illuminance_raw" + for high, low in data: + self.hw.inject(REG_ALS_RESULT_HIGH, high) + self.hw.inject(REG_ALS_RESULT_LOW, low) + self.hw.set_bit(REG_COMMAND, REG_COMMAND_ALS_DATA_RDY) + + self.assertEqual(read_int(luxfile), high << 8 | low) + + def test_lux_timeout(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + with self.assertRaises(OSError) as cm: + luxfile =3D dev.path / "iio:device0/in_illuminance_raw" + read_str(luxfile) + self.assertEqual(cm.exception.errno, errno.EIO) + + def test_proximity_thresh_rising(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + high_thresh =3D ( + dev.path / "iio:device0/events/in_proximity_thresh_rising_= value" + ) + write_int(high_thresh, 0x1234) + + mock =3D self.hw.update_mock() + mock.assert_last_reg_write(self, REG_HIGH_THRESHOLD_HIGH, 0x12) + mock.assert_last_reg_write(self, REG_HIGH_THRESHOLD_LOW, 0x34) + mock.reset_mock() + + self.assertEqual(read_int(high_thresh), 0x1234) + + with iio.IIOEventMonitor("/dev/iio:device0") as mon: + en =3D dev.path / "iio:device0/events/in_proximity_thresh_= either_en" + write_int(en, 1) + + self.hw.inject(REG_PROX_RESULT_HIGH, 0x12) + self.hw.inject(REG_PROX_RESULT_LOW, 0x35) + self.hw.set_bit(REG_COMMAND, REG_COMMAND_PROX_DATA_RDY) + self.hw.kick() + + self.assertEqual(read_int(en), 1) + + event =3D mon.read() + self.assertEqual(event.type, iio.IIOChanType.IIO_PROXIMITY) + + def test_proximity_thresh_falling(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + high_thresh =3D ( + dev.path / "iio:device0/events/in_proximity_thresh_falling= _value" + ) + write_int(high_thresh, 0x0ABC) + + mock =3D self.hw.update_mock() + mock.assert_last_reg_write(self, REG_LOW_THRESHOLD_HIGH, 0x0A) + mock.assert_last_reg_write(self, REG_LOW_THRESHOLD_LOW, 0xBC) + mock.reset_mock() + + self.assertEqual(read_int(high_thresh), 0x0ABC) + + with iio.IIOEventMonitor("/dev/iio:device0") as mon: + write_int( + dev.path / "iio:device0/events/in_proximity_thresh_eit= her_en", 1 + ) + + event =3D mon.read() + self.assertEqual(event.type, iio.IIOChanType.IIO_PROXIMITY) + + def test_proximity_triggered(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + data =3D [ + (0x00, 0x00, 0), + (0x00, 0x01, 1), + (0xF0, 0x02, 0xF002), + (0xFF, 0xFF, 0xFFFF), + ] + + trigger =3D read_str(Path("/sys/bus/iio/devices/trigger0/name"= )) + + write_int(dev.path / "iio:device0/buffer0/in_proximity_en", 1) + write_str(dev.path / "iio:device0/trigger/current_trigger", tr= igger) + + with iio.IIOBuffer("/dev/iio:device0", bufidx=3D0) as buffer: + write_int(dev.path / "iio:device0/buffer0/length", 128) + write_int(dev.path / "iio:device0/buffer0/enable", 1) + + for low, high, expected in data: + self.hw.inject(REG_PROX_RESULT_HIGH, low) + self.hw.inject(REG_PROX_RESULT_LOW, high) + self.hw.set_bit(REG_COMMAND, REG_COMMAND_PROX_DATA_RDY) + self.hw.kick() + + scanline =3D buffer.read("H") + + val =3D scanline[0] + self.assertEqual(val, expected) diff --git a/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4040.= py b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4040.py new file mode 100644 index 000000000000..f2aa2cb9f3d5 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4040.py @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import logging +from typing import Any + +from roadtest.backend.i2c import SMBusModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod, rmmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import I2CDriver, read_float, read_int + +logger =3D logging.getLogger(__name__) + + +class VCNL4040(SMBusModel): + def __init__(self, **kwargs: Any) -> None: + super().__init__(regbytes=3D2, byteorder=3D"little", **kwargs) + self.regs =3D { + 0x00: 0x0101, + 0x01: 0x0000, + 0x02: 0x0000, + 0x03: 0x0001, + 0x04: 0x0000, + 0x05: 0x0000, + 0x06: 0x0000, + 0x07: 0x0000, + 0x08: 0x0000, + 0x09: 0x0000, + 0x0A: 0x0000, + 0x0A: 0x0000, + 0x0B: 0x0000, + 0x0C: 0x0186, + # The driver reads this register which is undefined for + # VCNL4040. Perhaps the driver should be fixed instead + # of having this here? + 0x0E: 0x0000, + } + + def reg_read(self, addr: int) -> int: + return self.regs[addr] + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + self.regs[addr] =3D val + + +class TestVCNL4040(UMLTestCase): + dts =3D DtFragment( + src=3D""" +&i2c { + light-sensor@$addr$ { + compatible =3D "vishay,vcnl4040"; + reg =3D <0x$addr$>; + }; +}; + """, + variables=3D{ + "addr": DtVar.I2C_ADDR, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("vcnl4000") + + @classmethod + def tearDownClass(cls) -> None: + rmmod("vcnl4000") + + def setUp(self) -> None: + self.driver =3D I2CDriver("vcnl4000") + self.hw =3D Hardware("i2c") + self.hw.load_model(VCNL4040) + + def tearDown(self) -> None: + self.hw.close() + + def test_illuminance_scale(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + scalefile =3D dev.path / "iio:device0/in_illuminance_scale" + # The datasheet says 0.10 lux/step, but the driver follows + # the application note "Designing the VCNL4040 Into an + # Application" which claims a different value. + self.assertEqual(read_float(scalefile), 0.12) + + def test_illuminance(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + luxfile =3D dev.path / "iio:device0/in_illuminance_raw" + + data =3D [0x0000, 0x1234, 0xFFFF] + for regval in data: + self.hw.reg_write(0x09, regval) + self.assertEqual(read_int(luxfile), regval) + + def test_proximity(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + rawfile =3D dev.path / "iio:device0/in_proximity_raw" + + data =3D [0x0000, 0x1234, 0xFFFF] + for regval in data: + self.hw.reg_write(0x08, regval) + self.assertEqual(read_int(rawfile), regval) diff --git a/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4200.= py b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4200.py new file mode 100644 index 000000000000..d1cf819e563e --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/iio/light/test_vcnl4200.py @@ -0,0 +1,96 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import logging +from typing import Any + +from roadtest.backend.i2c import SMBusModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod, rmmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import I2CDriver, read_float, read_int + +logger =3D logging.getLogger(__name__) + + +class VCNL4200(SMBusModel): + def __init__(self, **kwargs: Any) -> None: + super().__init__(regbytes=3D2, byteorder=3D"little", **kwargs) + self.regs =3D { + 0x00: 0x0101, + 0x01: 0x0000, + 0x02: 0x0000, + 0x03: 0x0001, + 0x04: 0x0000, + 0x05: 0x0000, + 0x06: 0x0000, + 0x07: 0x0000, + 0x08: 0x0000, + 0x09: 0x0000, + 0x0A: 0x0000, + 0x0D: 0x0000, + 0x0E: 0x1058, + } + + def reg_read(self, addr: int) -> int: + return self.regs[addr] + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + self.regs[addr] =3D val + + +class TestVCNL4200(UMLTestCase): + dts =3D DtFragment( + src=3D""" +&i2c { + light-sensor@$addr$ { + compatible =3D "vishay,vcnl4200"; + reg =3D <0x$addr$>; + }; +}; + """, + variables=3D{ + "addr": DtVar.I2C_ADDR, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("vcnl4000") + + @classmethod + def tearDownClass(cls) -> None: + rmmod("vcnl4000") + + def setUp(self) -> None: + self.driver =3D I2CDriver("vcnl4000") + self.hw =3D Hardware("i2c") + self.hw.load_model(VCNL4200) + + def tearDown(self) -> None: + self.hw.close() + + def test_illuminance_scale(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + scalefile =3D dev.path / "iio:device0/in_illuminance_scale" + self.assertEqual(read_float(scalefile), 0.024) + + def test_illuminance(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + luxfile =3D dev.path / "iio:device0/in_illuminance_raw" + + data =3D [0x0000, 0x1234, 0xFFFF] + for regval in data: + self.hw.reg_write(0x09, regval) + self.assertEqual(read_int(luxfile), regval) + + def test_proximity(self) -> None: + with self.driver.bind(self.dts["addr"]) as dev: + rawfile =3D dev.path / "iio:device0/in_proximity_raw" + + data =3D [0x0000, 0x1234, 0xFFFF] + for regval in data: + self.hw.reg_write(0x08, regval) + self.assertEqual(read_int(rawfile), regval) --=20 2.34.1 From nobody Tue Jun 23 03:06:51 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id B8C11C433FE for ; Fri, 11 Mar 2022 16:26:11 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1350432AbiCKQ1M (ORCPT ); Fri, 11 Mar 2022 11:27:12 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:38336 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350350AbiCKQ0g (ORCPT ); Fri, 11 Mar 2022 11:26:36 -0500 Received: from smtp2.axis.com (smtp2.axis.com [195.60.68.18]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id B71C21BA171; Fri, 11 Mar 2022 08:25:01 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015903; x=1678551903; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=sxaJ4mG3Y2RIBWG3Mim/tcF1834/P18PDxXFWE4XQiY=; b=S6cxUV352uZY6mEk/LFIbYpc7U3IlpNM/SuQGwlNiurHrKoN4p6Yll4q 5RyRmGmz8TxtO7roqPQ+iXQzcyCRJlKN0tA2SXGlQYeAMI+zrLLOFqhHS HwIRmyTFsdWTSdOBfUEXGALC+GMHd3hGocixn/Uvq1dKSzGl9rdvslvTR 9J6b/kqnUNvu+exzvpmQyb6I7pFef4CMFYtpMeiRVnoK0sUT8w7x97jrB mdtpqfqfzew2ErgnEqgWHyoeugnUshKAUEo5tdeuoWjg08XTh35viln9u zQNWoDXHXitvt5GMWGW9vkV4r5z6sDjgb8blvQE7QGTsQ21xO2hCgNVE4 Q==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 09/10] regulator: tps62864: add roadtest Date: Fri, 11 Mar 2022 17:24:44 +0100 Message-ID: <20220311162445.346685-10-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Content-Type: text/plain; charset="utf-8" Add a roadtest for the recently-added tps62864 regulator driver. It tests voltage setting, mode setting, as well as devicetree mode translation. It uses the recently-added devicetree support in regulator-virtual-consumer. All the variants supported by the driver have identical register interfaces so only one test/model is added. It requires the following patches which are, as of writing, not in mainline: - regulator: Add support for TPS6286x https://lore.kernel.org/lkml/20220204155241.576342-3-vincent.whitchurch@= axis.com/ - regulator: virtual: add devicetree support https://lore.kernel.org/lkml/20220301111831.3742383-4-vincent.whitchurch= @axis.com/ Signed-off-by: Vincent Whitchurch --- .../roadtest/tests/regulator/__init__.py | 0 .../roadtest/roadtest/tests/regulator/config | 4 + .../roadtest/tests/regulator/test_tps62864.py | 187 ++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 tools/testing/roadtest/roadtest/tests/regulator/__init_= _.py create mode 100644 tools/testing/roadtest/roadtest/tests/regulator/config create mode 100644 tools/testing/roadtest/roadtest/tests/regulator/test_tp= s62864.py diff --git a/tools/testing/roadtest/roadtest/tests/regulator/__init__.py b/= tools/testing/roadtest/roadtest/tests/regulator/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/testing/roadtest/roadtest/tests/regulator/config b/tools= /testing/roadtest/roadtest/tests/regulator/config new file mode 100644 index 000000000000..b2b503947e70 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/regulator/config @@ -0,0 +1,4 @@ +CONFIG_REGULATOR=3Dy +CONFIG_REGULATOR_DEBUG=3Dy +CONFIG_REGULATOR_VIRTUAL_CONSUMER=3Dy +CONFIG_REGULATOR_TPS6286X=3Dm diff --git a/tools/testing/roadtest/roadtest/tests/regulator/test_tps62864.= py b/tools/testing/roadtest/roadtest/tests/regulator/test_tps62864.py new file mode 100644 index 000000000000..f7db4293d840 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/regulator/test_tps62864.py @@ -0,0 +1,187 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +from typing import Any, Final + +from roadtest.backend.i2c import SimpleSMBusModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod, rmmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import ( + I2CDriver, + PlatformDriver, + read_str, + write_int, + write_str, +) + +REG_VOUT1: Final =3D 0x01 +REG_VOUT2: Final =3D 0x02 +REG_CONTROL: Final =3D 0x03 +REG_STATUS: Final =3D 0x05 + + +class TPS62864(SimpleSMBusModel): + def __init__(self, **kwargs: Any) -> None: + super().__init__( + # From datasheet section 8.6 Register map + # XXX does not match reality -- recheck + regs=3D{ + REG_VOUT1: 0x64, + REG_VOUT2: 0x64, + REG_CONTROL: 0x00, + REG_STATUS: 0x00, + }, + regbytes=3D1, + **kwargs, + ) + + +class TestTPS62864(UMLTestCase): + dts =3D DtFragment( + src=3D""" +#include + +&i2c { + regulator@$normal$ { + compatible =3D "ti,tps62864"; + reg =3D <0x$normal$>; + + regulators { + tps62864_normal: SW { + regulator-name =3D "+0.85V"; + regulator-min-microvolt =3D <400000>; + regulator-max-microvolt =3D <1675000>; + regulator-allowed-modes =3D ; + }; + }; + }; + + regulator@$fpwm$ { + compatible =3D "ti,tps62864"; + reg =3D <0x$fpwm$>; + + regulators { + tps62864_fpwm: SW { + regulator-name =3D "+0.85V"; + regulator-min-microvolt =3D <400000>; + regulator-max-microvolt =3D <1675000>; + regulator-initial-mode =3D ; + }; + }; + }; +}; + +/ { + tps62864_normal_consumer { + compatible =3D "regulator-virtual-consumer"; + default-supply =3D <&tps62864_normal>; + }; + + tps62864_fpwm_consumer { + compatible =3D "regulator-virtual-consumer"; + default-supply =3D <&tps62864_fpwm>; + }; +}; + """, + variables=3D{ + "normal": DtVar.I2C_ADDR, + "fpwm": DtVar.I2C_ADDR, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("tps6286x-regulator") + + @classmethod + def tearDownClass(cls) -> None: + rmmod("tps6286x-regulator") + + def setUp(self) -> None: + self.driver =3D I2CDriver("tps6286x") + self.hw =3D Hardware("i2c") + self.hw.load_model(TPS62864) + + def tearDown(self) -> None: + self.hw.close() + + def test_voltage(self) -> None: + with ( + self.driver.bind(self.dts["normal"]), + PlatformDriver("reg-virt-consumer").bind( + "tps62864_normal_consumer" + ) as consumerdev, + ): + maxfile =3D consumerdev.path / "max_microvolts" + minfile =3D consumerdev.path / "min_microvolts" + + write_int(maxfile, 1675000) + write_int(minfile, 800000) + + mock =3D self.hw.update_mock() + mock.assert_reg_write_once(self, REG_CONTROL, 1 << 5) + mock.assert_reg_write_once(self, REG_VOUT1, 0x50) + mock.reset_mock() + + mV =3D 1000 + data =3D [ + (400 * mV, 0x00), + (900 * mV, 0x64), + (1675 * mV, 0xFF), + ] + + for voltage, val in data: + write_int(minfile, voltage) + mock =3D self.hw.update_mock() + mock.assert_reg_write_once(self, REG_VOUT1, val) + mock.reset_mock() + + write_int(minfile, 0) + mock =3D self.hw.update_mock() + mock.assert_reg_write_once(self, REG_CONTROL, 0) + mock.reset_mock() + + def test_modes(self) -> None: + with ( + self.driver.bind(self.dts["normal"]), + PlatformDriver("reg-virt-consumer").bind( + "tps62864_normal_consumer" + ) as consumerdev, + ): + modefile =3D consumerdev.path / "mode" + write_str(modefile, "fast") + + mock =3D self.hw.update_mock() + mock.assert_reg_write_once(self, REG_CONTROL, 1 << 4) + mock.reset_mock() + + write_str(modefile, "normal") + mock =3D self.hw.update_mock() + mock.assert_reg_write_once(self, REG_CONTROL, 0) + mock.reset_mock() + + def test_dt_force_pwm(self) -> None: + with ( + self.driver.bind(self.dts["fpwm"]), + PlatformDriver("reg-virt-consumer").bind( + "tps62864_fpwm_consumer" + ) as consumerdev, + ): + mock =3D self.hw.update_mock() + mock.assert_reg_write_once(self, REG_CONTROL, 1 << 4) + mock.reset_mock() + + modefile =3D consumerdev.path / "mode" + self.assertEquals(read_str(modefile), "fast") + + maxfile =3D consumerdev.path / "max_microvolts" + minfile =3D consumerdev.path / "min_microvolts" + + write_int(maxfile, 1675000) + write_int(minfile, 800000) + + mock =3D self.hw.update_mock() + mock.assert_reg_write_once(self, REG_CONTROL, 1 << 5 | 1 << 4) + mock.reset_mock() --=20 2.34.1 From nobody Tue Jun 23 03:06:51 2026 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id B9806C3525A for ; Fri, 11 Mar 2022 16:28:13 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1350333AbiCKQ3O (ORCPT ); Fri, 11 Mar 2022 11:29:14 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:38126 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1350386AbiCKQ0r (ORCPT ); Fri, 11 Mar 2022 11:26:47 -0500 Received: from smtp2.axis.com (smtp2.axis.com [195.60.68.18]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 22B3E1D21E0; Fri, 11 Mar 2022 08:25:10 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=axis.com; q=dns/txt; s=axis-central1; t=1647015914; x=1678551914; h=from:to:cc:subject:date:message-id:in-reply-to: references:mime-version:content-transfer-encoding; bh=mFUerXsYecLMbfF90aqK9kLZr/AsxHAsijhoN1YRpYE=; b=lQRk4LfR5AFqN9FEs7wrHk5cH5T9+y25qVLXzvBvAFYLBPTgRsjD1Yqh D2BB7TQBH8uN8bFQAZndlaN4Hj0QSlVPi7YuVlnEQzq7cFtz1tJ0G1F0N iim2PI/3jeCKvDVbVCa5gMFaQoMW0pfNcobauE8P1Q2CajDrQWsmsdCNz 2/1is0aMahXVPp6RQd2NPCBPAFkAx3q4uF1YXfCt0Y1p66ENX7//EakTL jzB8nQwB/wx6LXXn8fZaZEftxV+oYE44Xt83uFkmteszNsuBTv/sS9Lg1 XI1BEwDCGs3z+soJJ7z4sq02aLv1guD+2iG48gfEwTQT39ZJuBwgfIxn+ A==; From: Vincent Whitchurch To: CC: , Vincent Whitchurch , , , , , , , , , , , , , , Subject: [RFC v1 10/10] rtc: pcf8563: add roadtest Date: Fri, 11 Mar 2022 17:24:45 +0100 Message-ID: <20220311162445.346685-11-vincent.whitchurch@axis.com> X-Mailer: git-send-email 2.34.1 In-Reply-To: <20220311162445.346685-1-vincent.whitchurch@axis.com> References: <20220311162445.346685-1-vincent.whitchurch@axis.com> MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Precedence: bulk List-ID: X-Mailing-List: linux-kernel@vger.kernel.org Content-Type: text/plain; charset="utf-8" Add a roadtest for the PCF8563 RTC driver, testing many of the features including alarm and invalid time handling. Since it's the first roadtest for RTC, some helper code for handling the ABI is included. The following fixes were posted for problems identified during development of these tests: - rtc: fix use-after-free on device removal https://lore.kernel.org/lkml/20211210160951.7718-1-vincent.whitchurch@ax= is.com/ - rtc: pcf8563: clear RTC_FEATURE_ALARM if no irq https://lore.kernel.org/lkml/20220301131220.4011810-1-vincent.whitchurch= @axis.com/ - rtc: pcf8523: fix alarm interrupt disabling https://lore.kernel.org/lkml/20211103152253.22844-1-vincent.whitchurch@a= xis.com/ (not the same hardware/driver, but this was the original target for test development) Signed-off-by: Vincent Whitchurch --- .../roadtest/roadtest/tests/rtc/__init__.py | 0 .../roadtest/roadtest/tests/rtc/config | 1 + .../roadtest/roadtest/tests/rtc/rtc.py | 73 ++++ .../roadtest/tests/rtc/test_pcf8563.py | 348 ++++++++++++++++++ 4 files changed, 422 insertions(+) create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/__init__.py create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/config create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/rtc.py create mode 100644 tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.= py diff --git a/tools/testing/roadtest/roadtest/tests/rtc/__init__.py b/tools/= testing/roadtest/roadtest/tests/rtc/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/testing/roadtest/roadtest/tests/rtc/config b/tools/testi= ng/roadtest/roadtest/tests/rtc/config new file mode 100644 index 000000000000..f3654f9d7c19 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/rtc/config @@ -0,0 +1 @@ +CONFIG_RTC_DRV_PCF8563=3Dm diff --git a/tools/testing/roadtest/roadtest/tests/rtc/rtc.py b/tools/testi= ng/roadtest/roadtest/tests/rtc/rtc.py new file mode 100644 index 000000000000..1a2855bfc195 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/rtc/rtc.py @@ -0,0 +1,73 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import contextlib +import fcntl +import struct +import typing +from pathlib import Path +from typing import Any, cast + +RTC_RD_TIME =3D 0x80247009 +RTC_SET_TIME =3D 0x4024700A +RTC_WKALM_SET =3D 0x4028700F +RTC_VL_READ =3D 0x80047013 + +RTC_IRQF =3D 0x80 +RTC_AF =3D 0x20 + +RTC_VL_DATA_INVALID =3D 1 << 0 + + +class RTCTime(typing.NamedTuple): + tm_sec: int + tm_min: int + tm_hour: int + tm_mday: int + tm_mon: int + tm_year: int + tm_wday: int + tm_yday: int + tm_isdst: int + + +class RTC(contextlib.AbstractContextManager): + def __init__(self, devpath: Path) -> None: + rtc =3D next(devpath.glob("rtc/rtc*")).name + self.filename =3D f"/dev/{rtc}" + + def __enter__(self) -> "RTC": + self.file =3D open(self.filename, "rb") + return self + + def __exit__(self, *_: Any) -> None: + self.file.close() + + def read_time(self) -> RTCTime: + s =3D struct.Struct("9i") + buf =3D bytearray(s.size) + fcntl.ioctl(self.file.fileno(), RTC_RD_TIME, buf) + return RTCTime._make(s.unpack(buf)) + + def set_time(self, tm: RTCTime) -> int: + s =3D struct.Struct("9i") + buf =3D bytearray(s.size) + s.pack_into(buf, 0, *tm) + return fcntl.ioctl(self.file.fileno(), RTC_SET_TIME, buf) + + def set_wake_alarm(self, enabled: bool, time: RTCTime) -> int: + s =3D struct.Struct("2B9i") + buf =3D bytearray(s.size) + s.pack_into(buf, 0, enabled, False, *time) + return fcntl.ioctl(self.file.fileno(), RTC_WKALM_SET, buf) + + def read(self) -> int: + s =3D struct.Struct("L") + buf =3D self.file.read(s.size) + return cast(int, s.unpack(buf)[0]) + + def read_vl(self) -> int: + s =3D struct.Struct("I") + buf =3D bytearray(s.size) + fcntl.ioctl(self.file.fileno(), RTC_VL_READ, buf) + return cast(int, s.unpack(buf)[0]) diff --git a/tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py b/to= ols/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py new file mode 100644 index 000000000000..a9f4c6d92762 --- /dev/null +++ b/tools/testing/roadtest/roadtest/tests/rtc/test_pcf8563.py @@ -0,0 +1,348 @@ +# SPDX-License-Identifier: GPL-2.0-only +# Copyright Axis Communications AB + +import errno +import logging +from typing import Any, Final, Optional + +from roadtest.backend.i2c import I2CModel +from roadtest.core.devicetree import DtFragment, DtVar +from roadtest.core.hardware import Hardware +from roadtest.core.modules import insmod +from roadtest.core.suite import UMLTestCase +from roadtest.core.sysfs import I2CDriver + +from . import rtc + +logger =3D logging.getLogger(__name__) + +REG_CONTROL_STATUS_1: Final =3D 0x00 +REG_CONTROL_STATUS_2: Final =3D 0x01 +REG_VL_SECONDS: Final =3D 0x02 +REG_VL_MINUTES: Final =3D 0x03 +REG_VL_HOURS: Final =3D 0x04 +REG_VL_DAYS: Final =3D 0x05 +REG_VL_WEEKDAYS: Final =3D 0x06 +REG_VL_CENTURY_MONTHS: Final =3D 0x07 +REG_VL_YEARS: Final =3D 0x08 +REG_VL_MINUTE_ALARM: Final =3D 0x09 +REG_VL_HOUR_ALARM: Final =3D 0x0A +REG_VL_DAY_ALARM: Final =3D 0x0B +REG_VL_WEEKDAY_ALARM: Final =3D 0x0C +REG_CLKOUT_CONTROL: Final =3D 0x0D +REG_TIMER_CONTROL: Final =3D 0x0E +REG_TIMER: Final =3D 0x0F + +REG_CONTROL_STATUS_2_AIE: Final =3D 1 << 1 +REG_CONTROL_STATUS_2_AF: Final =3D 1 << 3 + +REG_VL_CENTURY_MONTHS_C: Final =3D 1 << 7 + +REG_VL_ALARM_AE: Final =3D 1 << 7 + + +class PCF8563(I2CModel): + def __init__(self, int: Optional[int] =3D None, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.int =3D int + self._set_int(False) + + self.reg_addr =3D 0 + # Reset values from Table 27 in datasheet, with X and - bits set t= o 0 + self.regs =3D { + REG_CONTROL_STATUS_1: 0b_0000_1000, + REG_CONTROL_STATUS_2: 0b_0000_0000, + REG_VL_SECONDS: 0b_1000_0000, + REG_VL_MINUTES: 0b_0000_0000, + REG_VL_HOURS: 0b_0000_0000, + REG_VL_DAYS: 0b_0000_0000, + REG_VL_WEEKDAYS: 0b_0000_0000, + REG_VL_CENTURY_MONTHS: 0b_0000_0000, + REG_VL_YEARS: 0b_0000_0000, + REG_VL_MINUTE_ALARM: 0b_1000_0000, + REG_VL_HOUR_ALARM: 0b_1000_0000, + REG_VL_DAY_ALARM: 0b_1000_0000, + REG_VL_WEEKDAY_ALARM: 0b_1000_0000, + REG_CLKOUT_CONTROL: 0b_1000_0000, + REG_TIMER_CONTROL: 0b_0000_0011, + REG_TIMER: 0b_0000_0000, + } + + def _set_int(self, active: int) -> None: + # Active-low + self.backend.gpio.set(self.int, not active) + + def _check_alarm(self, addr: int) -> None: + alarmregs =3D [ + REG_VL_MINUTE_ALARM, + REG_VL_HOUR_ALARM, + REG_VL_DAY_ALARM, + REG_VL_WEEKDAY_ALARM, + ] + timeregs =3D [ + REG_VL_MINUTES, + REG_VL_HOURS, + REG_VL_DAYS, + REG_VL_WEEKDAYS, + ] + + if addr not in alarmregs + timeregs: + return + + af =3D all( + self.regs[a] =3D=3D self.regs[b] + for a, b in zip(alarmregs, timeregs) + if not self.regs[a] & REG_VL_ALARM_AE + ) + self.reg_write(REG_CONTROL_STATUS_2, self.regs[REG_CONTROL_STATUS_= 2] | af << 3) + + def _update_irq(self) -> None: + aie =3D self.regs[REG_CONTROL_STATUS_2] & REG_CONTROL_STATUS_2_AIE + af =3D self.regs[REG_CONTROL_STATUS_2] & REG_CONTROL_STATUS_2_AF + + logger.debug(f"{aie=3D} {af=3D}") + self._set_int(aie and af) + + def reg_read(self, addr: int) -> int: + val =3D self.regs[addr] + return val + + def reg_write(self, addr: int, val: int) -> None: + assert addr in self.regs + self.regs[addr] =3D val + logger.debug(f"{addr=3D:x} {val=3D:x}") + self._check_alarm(addr) + self._update_irq() + + def read(self, len: int) -> bytes: + data =3D bytearray(len) + + for i in range(len): + data[i] =3D self.reg_read(self.reg_addr) + self.reg_addr =3D self.reg_addr + 1 + + return bytes(data) + + def write(self, data: bytes) -> None: + self.reg_addr =3D data[0] + + for i, byte in enumerate(data[1:]): + addr =3D self.reg_addr + i + self.backend.mock.reg_write(addr, byte) + self.reg_write(addr, byte) + + +class TestPCF8563(UMLTestCase): + dts =3D DtFragment( + src=3D""" +#include + +&i2c { + rtc@$addr$ { + compatible =3D "nxp,pcf8563"; + reg =3D <0x$addr$>; + }; + + rtc@$irqaddr$ { + compatible =3D "nxp,pcf8563"; + reg =3D <0x$irqaddr$>; + interrupt-parent =3D <&gpio>; + interrupts =3D <$gpio$ IRQ_TYPE_LEVEL_LOW>; + }; +}; + """, + variables=3D{ + "addr": DtVar.I2C_ADDR, + "irqaddr": DtVar.I2C_ADDR, + "gpio": DtVar.GPIO_PIN, + }, + ) + + @classmethod + def setUpClass(cls) -> None: + insmod("rtc-pcf8563") + + @classmethod + def tearDownClass(cls) -> None: + # Can't rmmod since alarmtimer holds permanent reference + pass + + def setUp(self) -> None: + self.driver =3D I2CDriver("rtc-pcf8563") + self.hw =3D Hardware("i2c") + self.hw.load_model(PCF8563, int=3Dself.dts["gpio"]) + + def tearDown(self) -> None: + self.hw.close() + + def test_read_time_invalid(self) -> None: + addr =3D self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + self.assertEqual(rtcdev.read_vl(), rtc.RTC_VL_DATA_INVALID) + + with self.assertRaises(OSError) as cm: + rtcdev.read_time() + self.assertEqual(cm.exception.errno, errno.EINVAL) + + def test_no_alarm_support(self) -> None: + addr =3D self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + # Make sure the times are valid so we don't get -EINVAL due to + # that. + tm =3D rtc.RTCTime( + tm_sec=3D10, + tm_min=3D1, + tm_hour=3D1, + tm_mday=3D1, + tm_mon=3D0, + tm_year=3D121, + tm_wday=3D0, + tm_yday=3D0, + tm_isdst=3D0, + ) + rtcdev.set_time(tm) + + alarmtm =3D tm._replace(tm_sec=3D0, tm_min=3D2) + with self.assertRaises(OSError) as cm: + rtcdev.set_wake_alarm(True, alarmtm) + self.assertEqual(cm.exception.errno, errno.EINVAL) + + def test_alarm(self) -> None: + addr =3D self.dts["irqaddr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + tm =3D rtc.RTCTime( + tm_sec=3D10, + tm_min=3D1, + tm_hour=3D1, + tm_mday=3D1, + tm_mon=3D0, + tm_year=3D121, + tm_wday=3D5, + tm_yday=3D0, + tm_isdst=3D0, + ) + rtcdev.set_time(tm) + + alarmtm =3D tm._replace(tm_sec=3D0, tm_min=3D2) + rtcdev.set_wake_alarm(True, alarmtm) + + mock =3D self.hw.update_mock() + mock.assert_last_reg_write(self, REG_VL_MINUTE_ALARM, 0x02) + mock.assert_last_reg_write(self, REG_VL_HOUR_ALARM, 0x01) + mock.assert_last_reg_write(self, REG_VL_DAY_ALARM, 0x01) + mock.assert_last_reg_write(self, REG_VL_WEEKDAY_ALARM, 5) + mock.assert_last_reg_write( + self, REG_CONTROL_STATUS_2, REG_CONTROL_STATUS_2_AIE + ) + mock.reset_mock() + + self.hw.reg_write(REG_VL_MINUTES, 0x02) + self.hw.kick() + + # This waits for the interrupt + self.assertEqual(rtcdev.read() & 0xFF, rtc.RTC_IRQF | rtc.RTC_= AF) + + alarmtm =3D tm._replace(tm_sec=3D0, tm_min=3D3) + rtcdev.set_wake_alarm(False, alarmtm) + + mock =3D self.hw.update_mock() + mock.assert_last_reg_write(self, REG_CONTROL_STATUS_2, 0) + + def test_read_time_valid(self) -> None: + self.hw.reg_write(REG_VL_SECONDS, 0x37) + self.hw.reg_write(REG_VL_MINUTES, 0x10) + self.hw.reg_write(REG_VL_HOURS, 0x11) + self.hw.reg_write(REG_VL_DAYS, 0x25) + self.hw.reg_write(REG_VL_WEEKDAYS, 0x00) + self.hw.reg_write(REG_VL_CENTURY_MONTHS, REG_VL_CENTURY_MONTHS_C |= 0x12) + self.hw.reg_write(REG_VL_YEARS, 0x21) + + addr =3D self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + tm =3D rtcdev.read_time() + self.assertEqual( + tm, + rtc.RTCTime( + tm_sec=3D37, + tm_min=3D10, + tm_hour=3D11, + tm_mday=3D25, + tm_mon=3D11, + tm_year=3D121, + tm_wday=3D0, + tm_yday=3D0, + tm_isdst=3D0, + ), + ) + + def test_set_time_after_invalid(self) -> None: + addr =3D self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + self.assertEqual(rtcdev.read_vl(), rtc.RTC_VL_DATA_INVALID) + + tm =3D rtc.RTCTime( + tm_sec=3D37, + tm_min=3D10, + tm_hour=3D11, + tm_mday=3D25, + tm_mon=3D11, + tm_year=3D121, + tm_wday=3D0, + tm_yday=3D0, + tm_isdst=3D0, + ) + + rtcdev.set_time(tm) + tm2 =3D rtcdev.read_time() + self.assertEqual(tm, tm2) + + mock =3D self.hw.update_mock() + mock.assert_reg_write_once(self, REG_VL_SECONDS, 0x37) + mock.assert_reg_write_once(self, REG_VL_MINUTES, 0x10) + mock.assert_reg_write_once(self, REG_VL_HOURS, 0x11) + mock.assert_reg_write_once(self, REG_VL_DAYS, 0x25) + mock.assert_reg_write_once(self, REG_VL_WEEKDAYS, 0x00) + # The driver uses the wrong polarity of the Century bit + # if the time was invalid. This probably doesn't matter(?). + mock.assert_reg_write_once(self, REG_VL_CENTURY_MONTHS, 0 << 7= | 0x12) + mock.assert_reg_write_once(self, REG_VL_YEARS, 0x21) + + self.assertEqual(rtcdev.read_vl(), 0) + + def test_set_time_after_valid(self) -> None: + self.hw.reg_write(REG_VL_SECONDS, 0x37) + self.hw.reg_write(REG_VL_MINUTES, 0x10) + self.hw.reg_write(REG_VL_HOURS, 0x11) + self.hw.reg_write(REG_VL_DAYS, 0x25) + self.hw.reg_write(REG_VL_WEEKDAYS, 0x00) + self.hw.reg_write(REG_VL_CENTURY_MONTHS, REG_VL_CENTURY_MONTHS_C |= 0x12) + self.hw.reg_write(REG_VL_YEARS, 0x21) + + addr =3D self.dts["addr"] + with self.driver.bind(addr) as dev, rtc.RTC(dev.path) as rtcdev: + tm =3D rtc.RTCTime( + tm_sec=3D37, + tm_min=3D10, + tm_hour=3D11, + tm_mday=3D25, + tm_mon=3D11, + tm_year=3D121, + tm_wday=3D0, + tm_yday=3D0, + tm_isdst=3D0, + ) + + rtcdev.set_time(tm) + tm2 =3D rtcdev.read_time() + self.assertEqual(tm, tm2) + + mock =3D self.hw.update_mock() + mock.assert_reg_write_once(self, REG_VL_SECONDS, 0x37) + mock.assert_reg_write_once(self, REG_VL_MINUTES, 0x10) + mock.assert_reg_write_once(self, REG_VL_HOURS, 0x11) + mock.assert_reg_write_once(self, REG_VL_DAYS, 0x25) + mock.assert_reg_write_once(self, REG_VL_WEEKDAYS, 0x00) + mock.assert_reg_write_once( + self, REG_VL_CENTURY_MONTHS, REG_VL_CENTURY_MONTHS_C | 0x12 + ) + mock.assert_reg_write_once(self, REG_VL_YEARS, 0x21) --=20 2.34.1