vmspawn: add integration test for QMP client library
Test the QMP client library using a mock QMP server over a socketpair: - test_qmp_client_basic: Verifies full handshake, query-status with response parsing, stop/cont commands, and asynchronous STOP event delivery via the sd-event I/O callback - test_qmp_client_eof: Verifies that the client properly detects server disconnection (EOF) and returns a disconnect error Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
This commit is contained in:
@@ -431,6 +431,9 @@ executables += [
|
||||
test_template + {
|
||||
'sources' : files('test-progress-bar.c'),
|
||||
},
|
||||
test_template + {
|
||||
'sources' : files('test-qmp-client.c'),
|
||||
},
|
||||
test_template + {
|
||||
'sources' : files('test-qrcode-util.c'),
|
||||
'dependencies' : libdl,
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "sd-event.h"
|
||||
#include "sd-json.h"
|
||||
|
||||
#include "errno-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "io-util.h"
|
||||
#include "pidref.h"
|
||||
#include "process-util.h"
|
||||
#include "qmp-client.h"
|
||||
#include "socket-util.h"
|
||||
#include "string-util.h"
|
||||
#include "tests.h"
|
||||
|
||||
/* Mock QMP server: runs in the child process of a fork, communicates via one end of a socketpair. */
|
||||
|
||||
static void mock_qmp_write_json(int fd, sd_json_variant *v) {
|
||||
_cleanup_free_ char *s = NULL;
|
||||
|
||||
ASSERT_OK(sd_json_variant_format(v, 0, &s));
|
||||
ASSERT_NOT_NULL(strextend(&s, "\r\n"));
|
||||
ASSERT_OK(loop_write(fd, s, SIZE_MAX));
|
||||
}
|
||||
|
||||
static void mock_qmp_write_literal(int fd, const char *msg) {
|
||||
ASSERT_OK(loop_write(fd, msg, SIZE_MAX));
|
||||
ASSERT_OK(loop_write(fd, "\r\n", 2));
|
||||
}
|
||||
|
||||
/* Read a command from the QMP client, verify it contains the expected command name, extract the id,
|
||||
* and send a reply with that id. If reply_data is NULL, an empty return object is sent. */
|
||||
static void mock_qmp_expect_and_reply(int fd, const char *expected_command, sd_json_variant *reply_data) {
|
||||
_cleanup_free_ char *buf = NULL;
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *cmd = NULL, *reply_obj = NULL, *response = NULL;
|
||||
|
||||
buf = ASSERT_NOT_NULL(new(char, 4096));
|
||||
|
||||
ssize_t n = read(fd, buf, 4095);
|
||||
assert_se(n > 0);
|
||||
buf[n] = '\0';
|
||||
|
||||
ASSERT_OK(sd_json_parse(buf, 0, &cmd, NULL, NULL));
|
||||
|
||||
sd_json_variant *execute = ASSERT_NOT_NULL(sd_json_variant_by_key(cmd, "execute"));
|
||||
ASSERT_STREQ(sd_json_variant_string(execute), expected_command);
|
||||
|
||||
sd_json_variant *id = ASSERT_NOT_NULL(sd_json_variant_by_key(cmd, "id"));
|
||||
|
||||
if (!reply_data)
|
||||
ASSERT_OK(sd_json_variant_new_object(&reply_obj, NULL, 0));
|
||||
|
||||
ASSERT_OK(sd_json_buildo(
|
||||
&response,
|
||||
SD_JSON_BUILD_PAIR("return", SD_JSON_BUILD_VARIANT(reply_data ?: reply_obj)),
|
||||
SD_JSON_BUILD_PAIR("id", SD_JSON_BUILD_VARIANT(id))));
|
||||
|
||||
mock_qmp_write_json(fd, response);
|
||||
}
|
||||
|
||||
static _noreturn_ void mock_qmp_server(int fd) {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *status_return = NULL;
|
||||
|
||||
/* Send QMP greeting */
|
||||
mock_qmp_write_literal(fd,
|
||||
"{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 2, \"major\": 9}}, \"capabilities\": [\"oob\"]}}");
|
||||
|
||||
/* Accept qmp_capabilities */
|
||||
mock_qmp_expect_and_reply(fd, "qmp_capabilities", NULL);
|
||||
|
||||
/* Accept query-status, reply with running state */
|
||||
ASSERT_OK(sd_json_buildo(
|
||||
&status_return,
|
||||
SD_JSON_BUILD_PAIR_BOOLEAN("running", true),
|
||||
SD_JSON_BUILD_PAIR_STRING("status", "running")));
|
||||
mock_qmp_expect_and_reply(fd, "query-status", status_return);
|
||||
|
||||
/* Accept stop */
|
||||
mock_qmp_expect_and_reply(fd, "stop", NULL);
|
||||
|
||||
/* Send a STOP event */
|
||||
mock_qmp_write_literal(fd,
|
||||
"{\"event\": \"STOP\", \"timestamp\": {\"seconds\": 1234, \"microseconds\": 5678}}");
|
||||
|
||||
/* Accept cont */
|
||||
mock_qmp_expect_and_reply(fd, "cont", NULL);
|
||||
|
||||
/* Close to trigger EOF */
|
||||
safe_close(fd);
|
||||
_exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
/* Test helper: tracks an async QMP command result and signals completion. */
|
||||
typedef struct {
|
||||
sd_json_variant *result;
|
||||
char *error_desc;
|
||||
int error;
|
||||
bool done;
|
||||
} QmpTestResult;
|
||||
|
||||
static int on_test_result(
|
||||
QmpClient *client,
|
||||
sd_json_variant *result,
|
||||
const char *error_desc,
|
||||
int error,
|
||||
void *userdata) {
|
||||
|
||||
QmpTestResult *t = ASSERT_PTR(userdata);
|
||||
|
||||
t->error = error;
|
||||
if (result)
|
||||
t->result = sd_json_variant_ref(result);
|
||||
if (error_desc)
|
||||
t->error_desc = strdup(error_desc);
|
||||
t->done = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Run the event loop until the test result callback fires. */
|
||||
static void qmp_test_wait(sd_event *event, QmpTestResult *t) {
|
||||
assert(event);
|
||||
assert(t);
|
||||
|
||||
while (!t->done)
|
||||
ASSERT_OK(sd_event_run(event, UINT64_MAX));
|
||||
}
|
||||
|
||||
static void qmp_test_result_done(QmpTestResult *t) {
|
||||
assert(t);
|
||||
|
||||
sd_json_variant_unref(t->result);
|
||||
free(t->error_desc);
|
||||
*t = (QmpTestResult) {};
|
||||
}
|
||||
|
||||
static int test_event_callback(
|
||||
QmpClient *client,
|
||||
const char *event,
|
||||
sd_json_variant *data,
|
||||
void *userdata) {
|
||||
|
||||
bool *event_received = ASSERT_PTR(userdata);
|
||||
|
||||
/* We may also receive a synthetic SHUTDOWN event when the mock server closes the connection;
|
||||
* only validate the STOP event we actually care about. */
|
||||
if (streq(event, "STOP"))
|
||||
*event_received = true;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
TEST(qmp_client_basic) {
|
||||
_cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
|
||||
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
|
||||
_cleanup_(pidref_done) PidRef pid = PIDREF_NULL;
|
||||
QmpTestResult t = {};
|
||||
sd_json_variant *running, *status;
|
||||
int qmp_fds[2];
|
||||
int r;
|
||||
|
||||
ASSERT_OK(sd_event_new(&event));
|
||||
|
||||
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
|
||||
|
||||
r = pidref_safe_fork("(mock-qmp)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid);
|
||||
ASSERT_OK(r);
|
||||
|
||||
if (r == 0) {
|
||||
safe_close(qmp_fds[0]);
|
||||
mock_qmp_server(qmp_fds[1]);
|
||||
}
|
||||
|
||||
safe_close(qmp_fds[1]);
|
||||
|
||||
/* Connect then attach to event loop — handshake completes transparently
|
||||
* inside the first call()/invoke(). */
|
||||
ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
|
||||
ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
|
||||
|
||||
/* Set event callback to catch STOP event during cont */
|
||||
bool event_received = false;
|
||||
qmp_client_bind_event(client, test_event_callback, &event_received);
|
||||
|
||||
/* Execute query-status */
|
||||
ASSERT_OK(qmp_client_invoke(client, "query-status", NULL, on_test_result, &t));
|
||||
qmp_test_wait(event, &t);
|
||||
ASSERT_EQ(t.error, 0);
|
||||
ASSERT_NOT_NULL(t.result);
|
||||
|
||||
running = ASSERT_NOT_NULL(sd_json_variant_by_key(t.result, "running"));
|
||||
ASSERT_TRUE(sd_json_variant_boolean(running));
|
||||
|
||||
status = ASSERT_NOT_NULL(sd_json_variant_by_key(t.result, "status"));
|
||||
ASSERT_STREQ(sd_json_variant_string(status), "running");
|
||||
|
||||
qmp_test_result_done(&t);
|
||||
|
||||
/* Execute stop */
|
||||
ASSERT_OK(qmp_client_invoke(client, "stop", NULL, on_test_result, &t));
|
||||
qmp_test_wait(event, &t);
|
||||
ASSERT_EQ(t.error, 0);
|
||||
qmp_test_result_done(&t);
|
||||
|
||||
/* Execute cont -- the STOP event should be dispatched by the IO callback */
|
||||
ASSERT_OK(qmp_client_invoke(client, "cont", NULL, on_test_result, &t));
|
||||
qmp_test_wait(event, &t);
|
||||
ASSERT_EQ(t.error, 0);
|
||||
qmp_test_result_done(&t);
|
||||
|
||||
/* Verify the STOP event was received */
|
||||
ASSERT_TRUE(event_received);
|
||||
|
||||
/* Wait for child and verify clean exit */
|
||||
siginfo_t si = {};
|
||||
ASSERT_OK(pidref_wait_for_terminate(&pid, &si));
|
||||
ASSERT_EQ(si.si_code, CLD_EXITED);
|
||||
ASSERT_EQ(si.si_status, EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
TEST(qmp_client_eof) {
|
||||
_cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
|
||||
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
|
||||
_cleanup_(pidref_done) PidRef pid = PIDREF_NULL;
|
||||
QmpTestResult t = {};
|
||||
int qmp_fds[2];
|
||||
int r;
|
||||
|
||||
ASSERT_OK(sd_event_new(&event));
|
||||
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
|
||||
|
||||
r = pidref_safe_fork("(mock-qmp-eof)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid);
|
||||
ASSERT_OK(r);
|
||||
|
||||
if (r == 0) {
|
||||
safe_close(qmp_fds[0]);
|
||||
|
||||
/* Send greeting and accept capabilities, then die */
|
||||
mock_qmp_write_literal(qmp_fds[1],
|
||||
"{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 0, \"major\": 9}}, \"capabilities\": []}}");
|
||||
|
||||
mock_qmp_expect_and_reply(qmp_fds[1], "qmp_capabilities", NULL);
|
||||
|
||||
/* Close immediately to trigger EOF */
|
||||
safe_close(qmp_fds[1]);
|
||||
_exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
safe_close(qmp_fds[1]);
|
||||
|
||||
ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
|
||||
ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
|
||||
|
||||
/* Executing a command should fail with a disconnect error because the server
|
||||
* closed. The handshake may succeed or fail inside invoke() — either way the
|
||||
* invoke itself or the async callback should report a disconnect. */
|
||||
r = qmp_client_invoke(client, "query-status", NULL, on_test_result, &t);
|
||||
if (r < 0)
|
||||
ASSERT_TRUE(ERRNO_IS_NEG_DISCONNECT(r));
|
||||
else {
|
||||
qmp_test_wait(event, &t);
|
||||
ASSERT_TRUE(ERRNO_IS_NEG_DISCONNECT(t.error));
|
||||
qmp_test_result_done(&t);
|
||||
}
|
||||
|
||||
siginfo_t si = {};
|
||||
ASSERT_OK(pidref_wait_for_terminate(&pid, &si));
|
||||
ASSERT_EQ(si.si_code, CLD_EXITED);
|
||||
ASSERT_EQ(si.si_status, EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
/* Read one QMP command from fd (one recvmsg, expecting it fits in the buffer for typical
|
||||
* test commands). Returns the number of SCM_RIGHTS fds that arrived attached to the read,
|
||||
* stores the first received fd in *ret_received_fd (or -EBADF if none) and closes any extras,
|
||||
* and parses the JSON into *ret_cmd. */
|
||||
static size_t mock_qmp_recv_command(int fd, sd_json_variant **ret_cmd, int *ret_received_fd) {
|
||||
char buf[4096];
|
||||
char ctrl[CMSG_SPACE(sizeof(int) * 4)];
|
||||
struct iovec iov = { .iov_base = buf, .iov_len = sizeof(buf) - 1 };
|
||||
struct msghdr mh = {
|
||||
.msg_iov = &iov, .msg_iovlen = 1,
|
||||
.msg_control = ctrl, .msg_controllen = sizeof(ctrl),
|
||||
};
|
||||
size_t n_fds = 0;
|
||||
int received_fd = -EBADF;
|
||||
|
||||
ssize_t n = recvmsg(fd, &mh, MSG_CMSG_CLOEXEC);
|
||||
assert_se(n > 0);
|
||||
buf[n] = '\0';
|
||||
|
||||
struct cmsghdr *cmsg;
|
||||
CMSG_FOREACH(cmsg, &mh) {
|
||||
if (cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS)
|
||||
continue;
|
||||
size_t k = (cmsg->cmsg_len - CMSG_LEN(0)) / sizeof(int);
|
||||
int *fds = (int*) CMSG_DATA(cmsg);
|
||||
for (size_t i = 0; i < k; i++) {
|
||||
if (received_fd < 0)
|
||||
received_fd = fds[i];
|
||||
else
|
||||
safe_close(fds[i]);
|
||||
}
|
||||
n_fds += k;
|
||||
}
|
||||
|
||||
ASSERT_OK(sd_json_parse(buf, 0, ret_cmd, NULL, NULL));
|
||||
|
||||
if (ret_received_fd)
|
||||
*ret_received_fd = received_fd;
|
||||
else if (received_fd >= 0)
|
||||
safe_close(received_fd);
|
||||
|
||||
return n_fds;
|
||||
}
|
||||
|
||||
/* Mock QMP server for the fd-on-first-invoke regression. Drives the wire dance:
|
||||
* greeting → (recv qmp_capabilities, expect 0 fds) → reply →
|
||||
* (recv add-fd, expect exactly 1 fd) → reply
|
||||
* Asserts the cmsg fd counts directly so a regression flips the child to
|
||||
* exit_failure and the parent test fails on the wait-for-terminate. */
|
||||
static _noreturn_ void mock_qmp_server_fd_first(int fd) {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *cap_cmd = NULL,
|
||||
*addfd_cmd = NULL,
|
||||
*cap_reply = NULL,
|
||||
*addfd_return = NULL,
|
||||
*addfd_reply = NULL;
|
||||
size_t n_fds;
|
||||
int received_fd = -EBADF;
|
||||
|
||||
/* Greeting */
|
||||
mock_qmp_write_literal(fd,
|
||||
"{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 0, \"major\": 9}}, \"capabilities\": []}}");
|
||||
|
||||
/* Receive qmp_capabilities — must arrive with NO fds attached. */
|
||||
n_fds = mock_qmp_recv_command(fd, &cap_cmd, /* ret_received_fd= */ NULL);
|
||||
ASSERT_EQ(n_fds, (size_t) 0);
|
||||
ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(cap_cmd, "execute")), "qmp_capabilities");
|
||||
|
||||
sd_json_variant *cap_id = ASSERT_NOT_NULL(sd_json_variant_by_key(cap_cmd, "id"));
|
||||
ASSERT_OK(sd_json_buildo(
|
||||
&cap_reply,
|
||||
SD_JSON_BUILD_PAIR("return", SD_JSON_BUILD_EMPTY_OBJECT),
|
||||
SD_JSON_BUILD_PAIR("id", SD_JSON_BUILD_VARIANT(cap_id))));
|
||||
mock_qmp_write_json(fd, cap_reply);
|
||||
|
||||
/* Receive add-fd — must arrive with EXACTLY ONE fd attached. */
|
||||
n_fds = mock_qmp_recv_command(fd, &addfd_cmd, &received_fd);
|
||||
ASSERT_EQ(n_fds, (size_t) 1);
|
||||
ASSERT_TRUE(received_fd >= 0);
|
||||
ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(addfd_cmd, "execute")), "add-fd");
|
||||
safe_close(received_fd);
|
||||
|
||||
sd_json_variant *addfd_id = ASSERT_NOT_NULL(sd_json_variant_by_key(addfd_cmd, "id"));
|
||||
ASSERT_OK(sd_json_buildo(
|
||||
&addfd_return,
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("fdset-id", 0),
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("fd", 42)));
|
||||
ASSERT_OK(sd_json_buildo(
|
||||
&addfd_reply,
|
||||
SD_JSON_BUILD_PAIR("return", SD_JSON_BUILD_VARIANT(addfd_return)),
|
||||
SD_JSON_BUILD_PAIR("id", SD_JSON_BUILD_VARIANT(addfd_id))));
|
||||
mock_qmp_write_json(fd, addfd_reply);
|
||||
|
||||
safe_close(fd);
|
||||
_exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
/* Regression: pass an fd in the very first qmp_client_invoke() against a fresh client
|
||||
* (lazy-bootstrap state, handshake not yet done). The previous push_fd+invoke split would
|
||||
* stage the fd on the stream BEFORE qmp_client_ensure_running() drove the handshake; the
|
||||
* handshake's qmp_capabilities enqueue would then steal the staged fd onto its own
|
||||
* sendmsg. The new QmpClientArgs API stages fds inside invoke AFTER ensure_running, so
|
||||
* the fd lands on add-fd's sendmsg as it should. */
|
||||
TEST(qmp_client_first_invoke_with_fd) {
|
||||
_cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
|
||||
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
|
||||
_cleanup_(pidref_done) PidRef pid = PIDREF_NULL;
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL;
|
||||
_cleanup_close_ int fd_to_pass = -EBADF;
|
||||
QmpTestResult t = {};
|
||||
int qmp_fds[2];
|
||||
int r;
|
||||
|
||||
ASSERT_OK(sd_event_new(&event));
|
||||
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
|
||||
|
||||
r = pidref_safe_fork("(mock-qmp-fd-first)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid);
|
||||
ASSERT_OK(r);
|
||||
|
||||
if (r == 0) {
|
||||
safe_close(qmp_fds[0]);
|
||||
mock_qmp_server_fd_first(qmp_fds[1]);
|
||||
}
|
||||
|
||||
safe_close(qmp_fds[1]);
|
||||
|
||||
/* Open a real fd to pass — /dev/null is universally available. */
|
||||
fd_to_pass = open("/dev/null", O_RDWR|O_CLOEXEC);
|
||||
ASSERT_OK(fd_to_pass);
|
||||
|
||||
ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
|
||||
ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
|
||||
|
||||
/* Build add-fd args. The fdset-id value is irrelevant — the mock server only
|
||||
* cares that the fd arrived with the correct sendmsg. */
|
||||
ASSERT_OK(sd_json_buildo(&args, SD_JSON_BUILD_PAIR_UNSIGNED("fdset-id", 0)));
|
||||
|
||||
/* THIS is the previously-broken pattern: very first invoke against the client,
|
||||
* carrying an fd, with the handshake still pending. */
|
||||
ASSERT_OK(qmp_client_invoke(client, "add-fd",
|
||||
QMP_CLIENT_ARGS_FD(args, TAKE_FD(fd_to_pass)),
|
||||
on_test_result, &t));
|
||||
|
||||
qmp_test_wait(event, &t);
|
||||
ASSERT_EQ(t.error, 0);
|
||||
ASSERT_NOT_NULL(t.result);
|
||||
qmp_test_result_done(&t);
|
||||
|
||||
/* Wait for the mock server child. If it received fds in the wrong order it
|
||||
* exited via the test-assertion failure path and si.si_status will be non-zero. */
|
||||
siginfo_t si = {};
|
||||
ASSERT_OK(pidref_wait_for_terminate(&pid, &si));
|
||||
ASSERT_EQ(si.si_code, CLD_EXITED);
|
||||
ASSERT_EQ(si.si_status, EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
/* Regression: when qmp_client_invoke() fails before stage_fds runs (e.g.
|
||||
* ensure_running() returns -ENOTCONN because the peer closed mid-handshake), the
|
||||
* caller-supplied fds — already TAKE_FD()'d through QMP_CLIENT_ARGS_FD() — must be
|
||||
* closed inside invoke. Otherwise they leak. */
|
||||
TEST(qmp_client_invoke_failure_closes_fds) {
|
||||
_cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL;
|
||||
_cleanup_close_ int fd_to_pass = -EBADF;
|
||||
QmpTestResult t = {};
|
||||
int qmp_fds[2];
|
||||
int saved_fd_value;
|
||||
|
||||
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
|
||||
|
||||
/* Close the peer end immediately so ensure_running()'s read sees EOF and
|
||||
* the client transitions straight to DISCONNECTED inside the first invoke. */
|
||||
safe_close(qmp_fds[1]);
|
||||
|
||||
ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
|
||||
/* Deliberately do NOT attach to an event loop — invoke uses ensure_running()'s
|
||||
* synchronous process+wait pump for the handshake. */
|
||||
|
||||
fd_to_pass = open("/dev/null", O_RDWR|O_CLOEXEC);
|
||||
ASSERT_OK(fd_to_pass);
|
||||
saved_fd_value = fd_to_pass; /* remember the int value for the closed-check */
|
||||
|
||||
ASSERT_OK(sd_json_buildo(&args, SD_JSON_BUILD_PAIR_UNSIGNED("fdset-id", 0)));
|
||||
|
||||
/* invoke must fail because the peer is gone. The TAKE_FD inside the macro
|
||||
* has already zeroed our local fd_to_pass; if invoke leaked the fd here,
|
||||
* the fd would stay open in our process. */
|
||||
int r = qmp_client_invoke(client, "add-fd",
|
||||
QMP_CLIENT_ARGS_FD(args, TAKE_FD(fd_to_pass)),
|
||||
on_test_result, &t);
|
||||
ASSERT_TRUE(r < 0);
|
||||
ASSERT_TRUE(ERRNO_IS_NEG_DISCONNECT(r));
|
||||
|
||||
/* fd_to_pass should now be -EBADF (TAKE_FD'd) and the underlying kernel fd
|
||||
* should have been closed by the qmp_client_args_close_fds cleanup in
|
||||
* qmp_client_invoke(). fcntl on the old int returns EBADF only if the slot
|
||||
* is genuinely free. */
|
||||
ASSERT_EQ(fd_to_pass, -EBADF);
|
||||
ASSERT_EQ(fcntl(saved_fd_value, F_GETFD), -1);
|
||||
ASSERT_EQ(errno, EBADF);
|
||||
}
|
||||
|
||||
TEST(qmp_schema_has_member) {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *schema = NULL;
|
||||
|
||||
/* QEMU introspection uses opaque numeric type ids ("0", "1", ...) — only member names are
|
||||
* the actual QAPI strings. Verify we walk all object entries and find the member by name. */
|
||||
ASSERT_OK(sd_json_build(&schema,
|
||||
SD_JSON_BUILD_ARRAY(
|
||||
SD_JSON_BUILD_OBJECT(
|
||||
SD_JSON_BUILD_PAIR_STRING("name", "0"),
|
||||
SD_JSON_BUILD_PAIR_STRING("meta-type", "object"),
|
||||
SD_JSON_BUILD_PAIR("members", SD_JSON_BUILD_ARRAY(
|
||||
SD_JSON_BUILD_OBJECT(
|
||||
SD_JSON_BUILD_PAIR_STRING("name", "offset"),
|
||||
SD_JSON_BUILD_PAIR_STRING("type", "int"))))),
|
||||
SD_JSON_BUILD_OBJECT(
|
||||
SD_JSON_BUILD_PAIR_STRING("name", "SomeEnum"),
|
||||
SD_JSON_BUILD_PAIR_STRING("meta-type", "enum")),
|
||||
SD_JSON_BUILD_OBJECT(
|
||||
SD_JSON_BUILD_PAIR_STRING("name", "1"),
|
||||
SD_JSON_BUILD_PAIR_STRING("meta-type", "object"),
|
||||
SD_JSON_BUILD_PAIR("members", SD_JSON_BUILD_ARRAY(
|
||||
SD_JSON_BUILD_OBJECT(
|
||||
SD_JSON_BUILD_PAIR_STRING("name", "lazy-refcounts"),
|
||||
SD_JSON_BUILD_PAIR_STRING("type", "bool")),
|
||||
SD_JSON_BUILD_OBJECT(
|
||||
SD_JSON_BUILD_PAIR_STRING("name", "discard-no-unref"),
|
||||
SD_JSON_BUILD_PAIR_STRING("type", "bool"))))))));
|
||||
|
||||
ASSERT_TRUE(qmp_schema_has_member(schema, "discard-no-unref"));
|
||||
ASSERT_TRUE(qmp_schema_has_member(schema, "offset"));
|
||||
ASSERT_FALSE(qmp_schema_has_member(schema, "definitely-not-a-real-field"));
|
||||
ASSERT_FALSE(qmp_schema_has_member(NULL, "discard-no-unref"));
|
||||
}
|
||||
|
||||
static int intro(void) {
|
||||
/* Ignore SIGPIPE so that write() to a closed socket returns EPIPE instead of killing us */
|
||||
ASSERT_TRUE(signal(SIGPIPE, SIG_IGN) != SIG_ERR);
|
||||
return 0;
|
||||
}
|
||||
|
||||
DEFINE_TEST_MAIN_FULL(LOG_DEBUG, intro, NULL);
|
||||
Reference in New Issue
Block a user