qtest: add test framework

The idea behind qtest is pretty simple.  Instead of executing a CPU via TCG or
KVM, rely on an external process to send events to the device model that the CPU
would normally generate.

qtest presents itself as an accelerator.  In addition, a new option is added to
establish a qtest server (-qtest) that takes a character device.  This is what
allows the external process to send CPU events to the device model.

qtest uses a simple line based protocol to send the events.  Documentation of
that protocol is in qtest.c.

I considered reusing the monitor for this job.  Adding interrupts would be a bit
difficult.  In addition, logging would also be difficult.

qtest has extensive logging support.  All protocol commands are logged with
time stamps using a new command line option (-qtest-log).  Logging is important
since ultimately, this is a feature for debugging.

Signed-off-by: Anthony Liguori <aliguori@us.ibm.com>
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
Signed-off-by: Anthony Liguori <aliguori@us.ibm.com>
This commit is contained in:
Anthony Liguori 2012-03-28 15:42:02 +02:00
parent b93b63f574
commit c7f0f3b1c8
8 changed files with 473 additions and 4 deletions

View File

@ -322,6 +322,8 @@ hw-obj-$(CONFIG_DP8393X) += dp8393x.o
hw-obj-$(CONFIG_DS1225Y) += ds1225y.o
hw-obj-$(CONFIG_MIPSNET) += mipsnet.o
hw-obj-y += qtest.o
# Sound
sound-obj-y =
sound-obj-$(CONFIG_SB16) += sb16.o

View File

@ -21,6 +21,7 @@
#include "disas.h"
#include "tcg.h"
#include "qemu-barrier.h"
#include "qtest.h"
int tb_invalidated_flag;

62
cpus.c
View File

@ -741,6 +741,48 @@ static void *qemu_kvm_cpu_thread_fn(void *arg)
return NULL;
}
static void *qemu_dummy_cpu_thread_fn(void *arg)
{
#ifdef _WIN32
fprintf(stderr, "qtest is not supported under Windows\n");
exit(1);
#else
CPUArchState *env = arg;
sigset_t waitset;
int r;
qemu_mutex_lock_iothread();
qemu_thread_get_self(env->thread);
env->thread_id = qemu_get_thread_id();
sigemptyset(&waitset);
sigaddset(&waitset, SIG_IPI);
/* signal CPU creation */
env->created = 1;
qemu_cond_signal(&qemu_cpu_cond);
cpu_single_env = env;
while (1) {
cpu_single_env = NULL;
qemu_mutex_unlock_iothread();
do {
int sig;
r = sigwait(&waitset, &sig);
} while (r == -1 && (errno == EAGAIN || errno == EINTR));
if (r == -1) {
perror("sigwait");
exit(1);
}
qemu_mutex_lock_iothread();
cpu_single_env = env;
qemu_wait_io_event_common(env);
}
return NULL;
#endif
}
static void tcg_exec_all(void);
static void *qemu_tcg_cpu_thread_fn(void *arg)
@ -803,7 +845,7 @@ void qemu_cpu_kick(void *_env)
CPUArchState *env = _env;
qemu_cond_broadcast(env->halt_cond);
if (kvm_enabled() && !env->thread_kicked) {
if (!tcg_enabled() && !env->thread_kicked) {
qemu_cpu_kick_thread(env);
env->thread_kicked = true;
}
@ -832,7 +874,7 @@ int qemu_cpu_is_self(void *_env)
void qemu_mutex_lock_iothread(void)
{
if (kvm_enabled()) {
if (!tcg_enabled()) {
qemu_mutex_lock(&qemu_global_mutex);
} else {
iothread_requesting_mutex = true;
@ -947,6 +989,18 @@ static void qemu_kvm_start_vcpu(CPUArchState *env)
}
}
static void qemu_dummy_start_vcpu(CPUArchState *env)
{
env->thread = g_malloc0(sizeof(QemuThread));
env->halt_cond = g_malloc0(sizeof(QemuCond));
qemu_cond_init(env->halt_cond);
qemu_thread_create(env->thread, qemu_dummy_cpu_thread_fn, env,
QEMU_THREAD_JOINABLE);
while (env->created == 0) {
qemu_cond_wait(&qemu_cpu_cond, &qemu_global_mutex);
}
}
void qemu_init_vcpu(void *_env)
{
CPUArchState *env = _env;
@ -956,8 +1010,10 @@ void qemu_init_vcpu(void *_env)
env->stopped = 1;
if (kvm_enabled()) {
qemu_kvm_start_vcpu(env);
} else {
} else if (tcg_enabled()) {
qemu_tcg_init_vcpu(env);
} else {
qemu_dummy_start_vcpu(env);
}
}

View File

@ -2715,6 +2715,14 @@ the @var{simple} tracing backend.
@end table
ETEXI
DEF("qtest", HAS_ARG, QEMU_OPTION_qtest,
"-qtest CHR specify tracing options\n",
QEMU_ARCH_ALL)
DEF("qtest-log", HAS_ARG, QEMU_OPTION_qtest_log,
"-qtest-log LOG specify tracing options\n",
QEMU_ARCH_ALL)
HXCOMM This is the last statement. Insert new options before this line!
STEXI
@end table

354
qtest.c Normal file
View File

@ -0,0 +1,354 @@
/*
* Test Server
*
* Copyright IBM, Corp. 2011
*
* Authors:
* Anthony Liguori <aliguori@us.ibm.com>
*
* This work is licensed under the terms of the GNU GPL, version 2 or later.
* See the COPYING file in the top-level directory.
*
*/
#include "qtest.h"
#include "qemu-char.h"
#include "ioport.h"
#include "memory.h"
#include "hw/irq.h"
#include "sysemu.h"
#define MAX_IRQ 256
const char *qtest_chrdev;
const char *qtest_log;
int qtest_allowed = 0;
static FILE *qtest_log_fp;
static CharDriverState *qtest_chr;
static GString *inbuf;
static int irq_levels[MAX_IRQ];
static struct timeval start_time;
static bool qtest_opened;
#define FMT_timeval "%" PRId64 ".%06" PRId64
/**
* QTest Protocol
*
* Line based protocol, request/response based. Server can send async messages
* so clients should always handle many async messages before the response
* comes in.
*
* Valid requests
*
* > outb ADDR VALUE
* < OK
*
* > outw ADDR VALUE
* < OK
*
* > outl ADDR VALUE
* < OK
*
* > inb ADDR
* < OK VALUE
*
* > inw ADDR
* < OK VALUE
*
* > inl ADDR
* < OK VALUE
*
* > read ADDR SIZE
* < OK DATA
*
* > write ADDR SIZE DATA
* < OK
*
* Valid async messages:
*
* IRQ raise NUM
* IRQ lower NUM
*
* ADDR, SIZE, VALUE are all integers parsed with strtoul() with a base of 0.
*
* DATA is an arbitrarily long hex number prefixed with '0x'. If it's smaller
* than the expected size, the value will be zero filled at the end of the data
* sequence.
*
* NUM is an IRQ number.
*/
static int hex2nib(char ch)
{
if (ch >= '0' && ch <= '9') {
return ch - '0';
} else if (ch >= 'a' && ch <= 'f') {
return 10 + (ch - 'a');
} else if (ch >= 'A' && ch <= 'F') {
return 10 + (ch - 'a');
} else {
return -1;
}
}
static void qtest_get_time(struct timeval *tv)
{
gettimeofday(tv, NULL);
tv->tv_sec -= start_time.tv_sec;
tv->tv_usec -= start_time.tv_usec;
if (tv->tv_usec < 0) {
tv->tv_usec += 1000000;
tv->tv_sec -= 1;
}
}
static void qtest_send_prefix(CharDriverState *chr)
{
struct timeval tv;
if (!qtest_log_fp || !qtest_opened) {
return;
}
qtest_get_time(&tv);
fprintf(qtest_log_fp, "[S +" FMT_timeval "] ",
tv.tv_sec, tv.tv_usec);
}
static void qtest_send(CharDriverState *chr, const char *fmt, ...)
{
va_list ap;
char buffer[1024];
size_t len;
va_start(ap, fmt);
len = vsnprintf(buffer, sizeof(buffer), fmt, ap);
va_end(ap);
qemu_chr_fe_write(chr, (uint8_t *)buffer, len);
if (qtest_log_fp && qtest_opened) {
fprintf(qtest_log_fp, "%s", buffer);
}
}
static void qtest_process_command(CharDriverState *chr, gchar **words)
{
const gchar *command;
g_assert(words);
command = words[0];
if (qtest_log_fp) {
struct timeval tv;
int i;
qtest_get_time(&tv);
fprintf(qtest_log_fp, "[R +" FMT_timeval "]",
tv.tv_sec, tv.tv_usec);
for (i = 0; words[i]; i++) {
fprintf(qtest_log_fp, " %s", words[i]);
}
fprintf(qtest_log_fp, "\n");
}
g_assert(command);
if (strcmp(words[0], "outb") == 0 ||
strcmp(words[0], "outw") == 0 ||
strcmp(words[0], "outl") == 0) {
uint16_t addr;
uint32_t value;
g_assert(words[1] && words[2]);
addr = strtol(words[1], NULL, 0);
value = strtol(words[2], NULL, 0);
if (words[0][3] == 'b') {
cpu_outb(addr, value);
} else if (words[0][3] == 'w') {
cpu_outw(addr, value);
} else if (words[0][3] == 'l') {
cpu_outl(addr, value);
}
qtest_send_prefix(chr);
qtest_send(chr, "OK\n");
} else if (strcmp(words[0], "inb") == 0 ||
strcmp(words[0], "inw") == 0 ||
strcmp(words[0], "inl") == 0) {
uint16_t addr;
uint32_t value = -1U;
g_assert(words[1]);
addr = strtol(words[1], NULL, 0);
if (words[0][2] == 'b') {
value = cpu_inb(addr);
} else if (words[0][2] == 'w') {
value = cpu_inw(addr);
} else if (words[0][2] == 'l') {
value = cpu_inl(addr);
}
qtest_send_prefix(chr);
qtest_send(chr, "OK 0x%04x\n", value);
} else if (strcmp(words[0], "read") == 0) {
uint64_t addr, len, i;
uint8_t *data;
g_assert(words[1] && words[2]);
addr = strtoul(words[1], NULL, 0);
len = strtoul(words[2], NULL, 0);
data = g_malloc(len);
cpu_physical_memory_read(addr, data, len);
qtest_send_prefix(chr);
qtest_send(chr, "OK 0x");
for (i = 0; i < len; i++) {
qtest_send(chr, "%02x", data[i]);
}
qtest_send(chr, "\n");
g_free(data);
} else if (strcmp(words[0], "write") == 0) {
uint64_t addr, len, i;
uint8_t *data;
size_t data_len;
g_assert(words[1] && words[2] && words[3]);
addr = strtoul(words[1], NULL, 0);
len = strtoul(words[2], NULL, 0);
data_len = strlen(words[3]);
if (data_len < 3) {
qtest_send(chr, "ERR invalid argument size\n");
return;
}
data = g_malloc(len);
for (i = 0; i < len; i++) {
if ((i * 2 + 4) <= data_len) {
data[i] = hex2nib(words[3][i * 2 + 2]) << 4;
data[i] |= hex2nib(words[3][i * 2 + 3]);
} else {
data[i] = 0;
}
}
cpu_physical_memory_write(addr, data, len);
g_free(data);
qtest_send_prefix(chr);
qtest_send(chr, "OK\n");
} else {
qtest_send_prefix(chr);
qtest_send(chr, "FAIL Unknown command `%s'\n", words[0]);
}
}
static void qtest_process_inbuf(CharDriverState *chr, GString *inbuf)
{
char *end;
while ((end = strchr(inbuf->str, '\n')) != NULL) {
size_t offset;
GString *cmd;
gchar **words;
offset = end - inbuf->str;
cmd = g_string_new_len(inbuf->str, offset);
g_string_erase(inbuf, 0, offset + 1);
words = g_strsplit(cmd->str, " ", 0);
qtest_process_command(chr, words);
g_strfreev(words);
g_string_free(cmd, TRUE);
}
}
static void qtest_read(void *opaque, const uint8_t *buf, int size)
{
CharDriverState *chr = opaque;
g_string_append_len(inbuf, (const gchar *)buf, size);
qtest_process_inbuf(chr, inbuf);
}
static int qtest_can_read(void *opaque)
{
return 1024;
}
static void qtest_event(void *opaque, int event)
{
int i;
switch (event) {
case CHR_EVENT_OPENED:
qemu_system_reset(false);
for (i = 0; i < ARRAY_SIZE(irq_levels); i++) {
irq_levels[i] = 0;
}
gettimeofday(&start_time, NULL);
qtest_opened = true;
if (qtest_log_fp) {
fprintf(qtest_log_fp, "[I " FMT_timeval "] OPENED\n",
start_time.tv_sec, start_time.tv_usec);
}
break;
case CHR_EVENT_CLOSED:
qtest_opened = false;
if (qtest_log_fp) {
struct timeval tv;
qtest_get_time(&tv);
fprintf(qtest_log_fp, "[I +" FMT_timeval "] CLOSED\n",
tv.tv_sec, tv.tv_usec);
}
break;
default:
break;
}
}
static void qtest_set_irq(void *opaque, int irq, int level)
{
CharDriverState *chr = qtest_chr;
bool changed;
changed = (irq_levels[irq] != level);
irq_levels[irq] = level;
if (changed) {
qtest_send_prefix(chr);
qtest_send(chr, "IRQ %s %d\n",
level ? "raise" : "lower", irq);
}
}
int qtest_init(void)
{
CharDriverState *chr;
g_assert(qtest_chrdev != NULL);
chr = qemu_chr_new("qtest", qtest_chrdev, NULL);
qemu_chr_add_handlers(chr, qtest_can_read, qtest_read, qtest_event, chr);
qemu_chr_fe_set_echo(chr, true);
inbuf = g_string_new("");
if (qtest_log) {
if (strcmp(qtest_log, "none") != 0) {
qtest_log_fp = fopen(qtest_log, "w+");
}
} else {
qtest_log_fp = stderr;
}
qtest_chr = chr;
return 0;
}

35
qtest.h Normal file
View File

@ -0,0 +1,35 @@
/*
* Test Server
*
* Copyright IBM, Corp. 2011
*
* Authors:
* Anthony Liguori <aliguori@us.ibm.com>
*
* 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 QTEST_H
#define QTEST_H
#include "qemu-common.h"
extern int qtest_allowed;
extern const char *qtest_chrdev;
extern const char *qtest_log;
static inline bool qtest_enabled(void)
{
return qtest_allowed;
}
static inline int qtest_available(void)
{
return 1;
}
int qtest_init(void);
#endif

5
scripts/qtest Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
export QTEST_QEMU_BINARY=$1
shift
"$@"

10
vl.c
View File

@ -152,6 +152,7 @@ int main(int argc, char **argv)
#ifdef CONFIG_VIRTFS
#include "fsdev/qemu-fsdev.h"
#endif
#include "qtest.h"
#include "disas.h"
@ -1312,7 +1313,7 @@ int qemu_shutdown_requested(void)
void qemu_kill_report(void)
{
if (shutdown_signal != -1) {
if (!qtest_enabled() && shutdown_signal != -1) {
fprintf(stderr, "qemu: terminating on signal %d", shutdown_signal);
if (shutdown_pid == 0) {
/* This happens for eg ^C at the terminal, so it's worth
@ -2098,6 +2099,7 @@ static struct {
{ "tcg", "tcg", tcg_available, tcg_init, &tcg_allowed },
{ "xen", "Xen", xen_available, xen_init, &xen_allowed },
{ "kvm", "KVM", kvm_available, kvm_init, &kvm_allowed },
{ "qtest", "QTest", qtest_available, qtest_init, &qtest_allowed },
};
static int configure_accelerator(void)
@ -3181,6 +3183,12 @@ int main(int argc, char **argv, char **envp)
fclose(fp);
break;
}
case QEMU_OPTION_qtest:
qtest_chrdev = optarg;
break;
case QEMU_OPTION_qtest_log:
qtest_log = optarg;
break;
default:
os_parse_cmd_args(popt->index, optarg);
}