[libvirt] [PATCH 0/3] Test case for fdstream / iohelper code

From: "Daniel P. Berrange" <berrange@redhat.com> There was a recent embarassing regression in the fdstream iohelper code which broke many commands/APIs. The problem code in question is quite easily testable via the unit tests. Daniel P. Berrange (3): Add a virGetLastErrorMessage() function Allow the iohelper path to be customized by test programs Add a test case for the fdstream file read/write code .gitignore | 1 + include/libvirt/virterror.h | 2 + src/fdstream.c | 14 +- src/fdstream.h | 3 + src/libvirt_private.syms | 1 + src/libvirt_public.syms | 5 + src/util/virerror.c | 21 +++ tests/Makefile.am | 5 + tests/fdstreamtest.c | 353 ++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 tests/fdstreamtest.c -- 1.8.2.1

From: "Daniel P. Berrange" <berrange@redhat.com> Apps using libvirt will often have code like if (virXXXX() < 0) { virErrorPtr err = virGetLastError(); fprintf(stderr, "Something failed: %s\n", err && err->message ? err->message : "unknown error"); return -1; } Checking for a NULL error object or message leads to very verbose code. A virGetLastErrorMessage() helper from libvirt can simplify this to if (virXXXX() < 0) { fprintf(stderr, "Something failed: %s\n", virGetLastErrorMessage()); return -1; } Signed-off-by: Daniel P. Berrange <berrange@redhat.com> --- include/libvirt/virterror.h | 2 ++ src/libvirt_public.syms | 5 +++++ src/util/virerror.c | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/include/libvirt/virterror.h b/include/libvirt/virterror.h index 3864a31..cd7e3b3 100644 --- a/include/libvirt/virterror.h +++ b/include/libvirt/virterror.h @@ -313,6 +313,8 @@ void virResetLastError (void); void virResetError (virErrorPtr err); void virFreeError (virErrorPtr err); +const char * virGetLastErrorMessage (void); + virErrorPtr virConnGetLastError (virConnectPtr conn); void virConnResetLastError (virConnectPtr conn); int virCopyLastError (virErrorPtr to); diff --git a/src/libvirt_public.syms b/src/libvirt_public.syms index dfbf44e..4ee2d27 100644 --- a/src/libvirt_public.syms +++ b/src/libvirt_public.syms @@ -616,4 +616,9 @@ LIBVIRT_1.0.5 { virNodeDeviceDetachFlags; } LIBVIRT_1.0.3; +LIBVIRT_1.0.6 { + global: + virGetLastErrorMessage; +} LIBVIRT_1.0.5; + # .... define new API here using predicted next version number .... diff --git a/src/util/virerror.c b/src/util/virerror.c index af4da8c..4e1dc9a 100644 --- a/src/util/virerror.c +++ b/src/util/virerror.c @@ -234,6 +234,27 @@ virGetLastError(void) return err; } + +/** + * virGetLastErrorMessage: + * + * Get the most recent error message + * + * Returns the most recent error message string in this + * thread, or a generic message if none is set + */ +const char * +virGetLastErrorMessage(void) +{ + virErrorPtr err = virLastErrorObject(); + if (!err || err->code == VIR_ERR_OK) + return _("no error"); + if (err->message == NULL) + return _("unknown error"); + return err->message; +} + + /** * virSetError: * @newerr: previously saved error object -- 1.8.2.1

On 05/10/2013 11:17 AM, Daniel P. Berrange wrote:
From: "Daniel P. Berrange" <berrange@redhat.com>
Apps using libvirt will often have code like
if (virXXXX() < 0) { virErrorPtr err = virGetLastError(); fprintf(stderr, "Something failed: %s\n", err && err->message ? err->message : "unknown error"); return -1; }
Checking for a NULL error object or message leads to very verbose code. A virGetLastErrorMessage() helper from libvirt can simplify this to
if (virXXXX() < 0) { fprintf(stderr, "Something failed: %s\n", virGetLastErrorMessage()); return -1; }
Signed-off-by: Daniel P. Berrange <berrange@redhat.com> --- include/libvirt/virterror.h | 2 ++ src/libvirt_public.syms | 5 +++++ src/util/virerror.c | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+)
ACK. Client-side only, so it doesn't need RPC support. Should we update examples/hellolibvirt/hellolibvirt.c to use it? -- Eric Blake eblake redhat com +1-919-301-3266 Libvirt virtualization library http://libvirt.org

On Fri, May 10, 2013 at 11:26:42AM -0600, Eric Blake wrote:
On 05/10/2013 11:17 AM, Daniel P. Berrange wrote:
From: "Daniel P. Berrange" <berrange@redhat.com>
Apps using libvirt will often have code like
if (virXXXX() < 0) { virErrorPtr err = virGetLastError(); fprintf(stderr, "Something failed: %s\n", err && err->message ? err->message : "unknown error"); return -1; }
Checking for a NULL error object or message leads to very verbose code. A virGetLastErrorMessage() helper from libvirt can simplify this to
if (virXXXX() < 0) { fprintf(stderr, "Something failed: %s\n", virGetLastErrorMessage()); return -1; }
Signed-off-by: Daniel P. Berrange <berrange@redhat.com> --- include/libvirt/virterror.h | 2 ++ src/libvirt_public.syms | 5 +++++ src/util/virerror.c | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+)
ACK. Client-side only, so it doesn't need RPC support.
Should we update examples/hellolibvirt/hellolibvirt.c to use it?
Good idea, I'll send another patch for that. Daniel -- |: http://berrange.com -o- http://www.flickr.com/photos/dberrange/ :| |: http://libvirt.org -o- http://virt-manager.org :| |: http://autobuild.org -o- http://search.cpan.org/~danberr/ :| |: http://entangle-photo.org -o- http://live.gnome.org/gtk-vnc :|

From: "Daniel P. Berrange" <berrange@redhat.com> Currently the fdstream function hardcodes the location of the iohelper to LIBEXECDIR "/libvirt_iohelper". This is not convenient when trying to write test cases which use this code. Add a virFDStreamSetIOHelper method to allow the test cases to point to the location of the un-installed iohelper binary. Signed-off-by: Daniel P. Berrange <berrange@redhat.com> --- src/fdstream.c | 14 +++++++++++++- src/fdstream.h | 3 +++ src/libvirt_private.syms | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/fdstream.c b/src/fdstream.c index a9a4851..d5e5aaf 100644 --- a/src/fdstream.c +++ b/src/fdstream.c @@ -74,6 +74,18 @@ struct virFDStreamData { virMutex lock; }; + +static const char *iohelper_path = LIBEXECDIR "/libvirt_iohelper"; + +void virFDStreamSetIOHelper(const char *path) +{ + if (path == NULL) + iohelper_path = LIBEXECDIR "/libvirt_iohelper"; + else + iohelper_path = path; +} + + static int virFDStreamRemoveCallback(virStreamPtr stream) { struct virFDStreamData *fdst = stream->privateData; @@ -634,7 +646,7 @@ virFDStreamOpenFileInternal(virStreamPtr st, goto error; } - cmd = virCommandNewArgList(LIBEXECDIR "/libvirt_iohelper", + cmd = virCommandNewArgList(iohelper_path, path, NULL); virCommandAddArgFormat(cmd, "%llu", length); diff --git a/src/fdstream.h b/src/fdstream.h index d6f5a7a..3ca6256 100644 --- a/src/fdstream.h +++ b/src/fdstream.h @@ -33,6 +33,9 @@ typedef void (*virFDStreamInternalCloseCb)(virStreamPtr st, void *opaque); typedef void (*virFDStreamInternalCloseCbFreeOpaque)(void *opaque); +/* Only for use by test suite */ +void virFDStreamSetIOHelper(const char *path); + int virFDStreamOpen(virStreamPtr st, int fd); diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms index bb70595..3cf0bcb 100644 --- a/src/libvirt_private.syms +++ b/src/libvirt_private.syms @@ -699,6 +699,7 @@ virFDStreamConnectUNIX; virFDStreamCreateFile; virFDStreamOpen; virFDStreamOpenFile; +virFDStreamSetIOHelper; # libvirt_internal.h -- 1.8.2.1

On 05/10/2013 11:17 AM, Daniel P. Berrange wrote:
From: "Daniel P. Berrange" <berrange@redhat.com>
Currently the fdstream function hardcodes the location of the iohelper to LIBEXECDIR "/libvirt_iohelper". This is not convenient when trying to write test cases which use this code. Add a virFDStreamSetIOHelper method to allow the test cases to point to the location of the un-installed iohelper binary.
Nice. Definitely shaves the need for a round trip 'make install' when testing changes. -- Eric Blake eblake redhat com +1-919-301-3266 Libvirt virtualization library http://libvirt.org

From: "Daniel P. Berrange" <berrange@redhat.com> Add a test case which exercises the virFDStreamOpenFile and virFDStreamCreateFile methods. Ensure that both the synchronous and non-blocking iohelper code paths work. This validates the regression recently fixed which broke reading in non-blocking mode Signed-off-by: Daniel P. Berrange <berrange@redhat.com> --- .gitignore | 1 + tests/Makefile.am | 5 + tests/fdstreamtest.c | 353 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 359 insertions(+) create mode 100644 tests/fdstreamtest.c diff --git a/.gitignore b/.gitignore index 5e50b52..e327b37 100644 --- a/.gitignore +++ b/.gitignore @@ -145,6 +145,7 @@ /tests/esxutilstest /tests/eventtest /tests/hashtest +/tests/fdstreamtest /tests/jsontest /tests/libvirtdconftest /tests/networkxml2argvtest diff --git a/tests/Makefile.am b/tests/Makefile.am index 68dbb27..68cdd53 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -107,6 +107,7 @@ test_programs = virshtest sockettest \ virportallocatortest \ sysinfotest \ virstoragetest \ + fdstreamtest \ $(NULL) if WITH_GNUTLS @@ -710,6 +711,10 @@ sysinfotest_SOURCES = \ sysinfotest.c testutils.h testutils.c sysinfotest_LDADD = $(LDADDS) +fdstreamtest_SOURCES = \ + fdstreamtest.c testutils.h testutils.c +fdstreamtest_LDADD = $(LDADDS) + if WITH_CIL CILOPTFLAGS = CILOPTINCS = diff --git a/tests/fdstreamtest.c b/tests/fdstreamtest.c new file mode 100644 index 0000000..aa68fb7 --- /dev/null +++ b/tests/fdstreamtest.c @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2013 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; If not, see + * <http://www.gnu.org/licenses/>. + * + * Author: Daniel P. Berrange <berrange@redhat.com> + */ + +#include <config.h> + +#include <stdlib.h> +#include <fcntl.h> + +#include "testutils.h" + +#include "fdstream.h" +#include "datatypes.h" +#include "virerror.h" +#include "viralloc.h" +#include "virlog.h" +#include "virstring.h" +#include "virfile.h" +#include "virutil.h" + +#define VIR_FROM_THIS VIR_FROM_NONE + +#define PATTERN_LEN 256 + +static int testFDStreamReadCommon(const char *scratchdir, bool blocking) +{ + int fd = -1; + char *tmpfile = NULL; + int ret = -1; + char *pattern = NULL; + char *buf = NULL; + virStreamPtr st = NULL; + size_t i; + virConnectPtr conn = NULL; + int flags = 0; + + if (!blocking) + flags |= VIR_STREAM_NONBLOCK; + + if (!(conn = virConnectOpen("test:///default"))) + goto cleanup; + + if (VIR_ALLOC_N(pattern, PATTERN_LEN) < 0 || + VIR_ALLOC_N(buf, PATTERN_LEN) < 0) + goto cleanup; + + for (i = 0 ; i < PATTERN_LEN ; i++) + pattern[i] = i; + + if (virAsprintf(&tmpfile, "%s/input.data", scratchdir) < 0) + goto cleanup; + + if ((fd = open(tmpfile, O_CREAT|O_WRONLY, 0600)) < 0) + goto cleanup; + + for (i = 0 ; i < 10 ; i++) { + if (safewrite(fd, pattern, PATTERN_LEN) != PATTERN_LEN) + goto cleanup; + } + + if (VIR_CLOSE(fd) < 0) + goto cleanup; + + if (!(st = virStreamNew(conn, flags))) + goto cleanup; + + /* Start reading 1/2 way through first pattern + * and end 1/2 way through last pattern + */ + if (virFDStreamOpenFile(st, tmpfile, + PATTERN_LEN / 2, PATTERN_LEN * 9, + O_RDONLY) < 0) + goto cleanup; + + for (i = 0 ; i < 10 ; i++) { + size_t offset = 0; + size_t want; + if (i == 0) + want = PATTERN_LEN / 2; + else + want = PATTERN_LEN; + + while (want > 0) { + int got; + reread: + got = st->driver->streamRecv(st, buf + offset, want); + if (got < 0) { + if (got == -2 && !blocking) { + usleep(20*1000); + goto reread; + } + fprintf(stderr, "Failed to read stream: %s\n", + virGetLastErrorMessage()); + goto cleanup; + } + if (got == 0) { + /* Expect EOF 1/2 through last pattern */ + if (i == 9 && want == (PATTERN_LEN / 2)) + break; + fprintf(stderr, "Unexpected EOF block %zu want %zu\n", + i, want); + goto cleanup; + } + offset += got; + want -= got; + } + if (i == 0) { + if (memcmp(buf, pattern + (PATTERN_LEN / 2), PATTERN_LEN / 2) != 0) { + fprintf(stderr, "Mismatched pattern data iteration %zu\n", i); + goto cleanup; + } + } else if (i == 9) { + if (memcmp(buf, pattern, PATTERN_LEN / 2) != 0) { + fprintf(stderr, "Mismatched pattern data iteration %zu\n", i); + goto cleanup; + } + } else { + if (memcmp(buf, pattern, PATTERN_LEN) != 0) { + fprintf(stderr, "Mismatched pattern data iteration %zu\n", i); + goto cleanup; + } + } + } + + if (st->driver->streamFinish(st) != 0) { + fprintf(stderr, "Failed to finish stream: %s\n", + virGetLastErrorMessage()); + goto cleanup; + } + + ret = 0; +cleanup: + if (st) + virStreamFree(st); + VIR_FORCE_CLOSE(fd); + if (tmpfile != NULL) + unlink(tmpfile); + if (conn) + virConnectClose(conn); + VIR_FREE(tmpfile); + VIR_FREE(pattern); + VIR_FREE(buf); + return ret; +} + + +static int testFDStreamReadBlock(const void *data) +{ + return testFDStreamReadCommon(data, true); +} +static int testFDStreamReadNonblock(const void *data) +{ + return testFDStreamReadCommon(data, false); +} + + +static int testFDStreamWriteCommon(const char *scratchdir, bool blocking) +{ + int fd = -1; + char *tmpfile = NULL; + int ret = -1; + char *pattern = NULL; + char *buf = NULL; + virStreamPtr st = NULL; + size_t i; + virConnectPtr conn = NULL; + int flags = 0; + + if (!blocking) + flags |= VIR_STREAM_NONBLOCK; + + if (!(conn = virConnectOpen("test:///default"))) + goto cleanup; + + if (VIR_ALLOC_N(pattern, PATTERN_LEN) < 0 || + VIR_ALLOC_N(buf, PATTERN_LEN) < 0) + goto cleanup; + + for (i = 0 ; i < PATTERN_LEN ; i++) + pattern[i] = i; + + if (virAsprintf(&tmpfile, "%s/input.data", scratchdir) < 0) + goto cleanup; + + if (!(st = virStreamNew(conn, flags))) + goto cleanup; + + /* Start writing 1/2 way through first pattern + * and end 1/2 way through last pattern + */ + if (virFDStreamCreateFile(st, tmpfile, + PATTERN_LEN / 2, PATTERN_LEN * 9, + O_WRONLY, 0600) < 0) + goto cleanup; + + for (i = 0 ; i < 10 ; i++) { + size_t offset = 0; + size_t want; + if (i == 0) + want = PATTERN_LEN / 2; + else + want = PATTERN_LEN; + + while (want > 0) { + int got; + rewrite: + got = st->driver->streamSend(st, pattern + offset, want); + if (got < 0) { + if (got == -2 && !blocking) { + usleep(20*1000); + goto rewrite; + } + if (i == 9 && + want == (PATTERN_LEN / 2)) + break; + fprintf(stderr, "Failed to write stream: %s\n", + virGetLastErrorMessage()); + goto cleanup; + } + offset += got; + want -= got; + } + } + + if (st->driver->streamFinish(st) != 0) { + fprintf(stderr, "Failed to finish stream: %s\n", + virGetLastErrorMessage()); + goto cleanup; + } + + if ((fd = open(tmpfile, O_RDONLY)) < 0) + goto cleanup; + + for (i = 0 ; i < 10 ; i++) { + size_t want; + if (i == 9) + want = PATTERN_LEN / 2; + else + want = PATTERN_LEN; + + if (saferead(fd, buf, want) != want) { + fprintf(stderr, "Short read from data\n"); + goto cleanup; + } + + if (i == 0) { + size_t j; + for (j = 0 ; j < (PATTERN_LEN / 2) ; j++) { + if (buf[j] != 0) { + fprintf(stderr, "Mismatched pattern data iteration %zu\n", i); + goto cleanup; + } + } + if (memcmp(buf + (PATTERN_LEN / 2), pattern, PATTERN_LEN / 2) != 0) { + fprintf(stderr, "Mismatched pattern data iteration %zu\n", i); + goto cleanup; + } + } else if (i == 9) { + if (memcmp(buf, pattern, PATTERN_LEN / 2) != 0) { + fprintf(stderr, "Mismatched pattern data iteration %zu\n", i); + goto cleanup; + } + } else { + if (memcmp(buf, pattern, PATTERN_LEN) != 0) { + fprintf(stderr, "Mismatched pattern data iteration %zu\n", i); + goto cleanup; + } + } + } + + if (VIR_CLOSE(fd) < 0) + goto cleanup; + + ret = 0; +cleanup: + if (st) + virStreamFree(st); + VIR_FORCE_CLOSE(fd); + if (tmpfile != NULL) + unlink(tmpfile); + if (conn) + virConnectClose(conn); + VIR_FREE(tmpfile); + VIR_FREE(pattern); + VIR_FREE(buf); + return ret; +} + + +static int testFDStreamWriteBlock(const void *data) +{ + return testFDStreamWriteCommon(data, true); +} +static int testFDStreamWriteNonblock(const void *data) +{ + return testFDStreamWriteCommon(data, false); +} + +# define SCRATCHDIRTEMPLATE abs_builddir "/fakesysfsdir-XXXXXX" + +static int +mymain(void) +{ + char *scratchdir; + int ret = 0; + const char *iohelper = abs_builddir "/../src/libvirt_iohelper"; + + virFDStreamSetIOHelper(iohelper); + + if (!(scratchdir = strdup(SCRATCHDIRTEMPLATE))) { + fprintf(stderr, "Out of memory\n"); + abort(); + } + + if (!mkdtemp(scratchdir)) { + fprintf(stderr, "Cannot create fakesysfsdir"); + abort(); + } + + if (virtTestRun("Stream read blocking ", 1, testFDStreamReadBlock, scratchdir) < 0) + ret = -1; + if (virtTestRun("Stream read non-blocking ", 1, testFDStreamReadNonblock, scratchdir) < 0) + ret = -1; + if (virtTestRun("Stream write blocking ", 1, testFDStreamWriteBlock, scratchdir) < 0) + ret = -1; + if (virtTestRun("Stream write non-blocking ", 1, testFDStreamWriteNonblock, scratchdir) < 0) + ret = -1; + + if (getenv("LIBVIRT_SKIP_CLEANUP") == NULL) + virFileDeleteTree(scratchdir); + + VIR_FREE(scratchdir); + + return ret==0 ? EXIT_SUCCESS : EXIT_FAILURE; +} + +VIRT_TEST_MAIN(mymain) -- 1.8.2.1

On 05/10/2013 11:17 AM, Daniel P. Berrange wrote:
From: "Daniel P. Berrange" <berrange@redhat.com>
Add a test case which exercises the virFDStreamOpenFile and virFDStreamCreateFile methods. Ensure that both the synchronous and non-blocking iohelper code paths work. This validates the regression recently fixed which broke reading in non-blocking mode
Always nice to enhance our testsuite to avoid future regressions.
Signed-off-by: Daniel P. Berrange <berrange@redhat.com> --- .gitignore | 1 + tests/Makefile.am | 5 + tests/fdstreamtest.c | 353 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 359 insertions(+) create mode 100644 tests/fdstreamtest.c
ACK with nits:
diff --git a/.gitignore b/.gitignore index 5e50b52..e327b37 100644 --- a/.gitignore +++ b/.gitignore @@ -145,6 +145,7 @@ /tests/esxutilstest /tests/eventtest /tests/hashtest +/tests/fdstreamtest
Sorting.
+++ b/tests/Makefile.am @@ -107,6 +107,7 @@ test_programs = virshtest sockettest \ virportallocatortest \ sysinfotest \ virstoragetest \ + fdstreamtest \
Space vs. TAB inconsistency (but you can argue some of it was pre-existing).
+ +static int testFDStreamReadCommon(const char *scratchdir, bool blocking) +{
+ if (virAsprintf(&tmpfile, "%s/input.data", scratchdir) < 0) + goto cleanup; + + if ((fd = open(tmpfile, O_CREAT|O_WRONLY, 0600)) < 0)
Worth using O_EXCL as well?
+ reread: + got = st->driver->streamRecv(st, buf + offset, want); + if (got < 0) { + if (got == -2 && !blocking) { + usleep(20*1000);
Spaces around *
+static int testFDStreamWriteCommon(const char *scratchdir, bool blocking) +{ + rewrite: + got = st->driver->streamSend(st, pattern + offset, want); + if (got < 0) { + if (got == -2 && !blocking) { + usleep(20*1000);
Spaces around *
+ +# define SCRATCHDIRTEMPLATE abs_builddir "/fakesysfsdir-XXXXXX" + +static int +mymain(void) +{ + char *scratchdir; + int ret = 0; + const char *iohelper = abs_builddir "/../src/libvirt_iohelper"; + + virFDStreamSetIOHelper(iohelper); + + if (!(scratchdir = strdup(SCRATCHDIRTEMPLATE))) { + fprintf(stderr, "Out of memory\n"); + abort(); + }
No need to use strdup, given our desire to switch to VIR_STRDUP. Why not just stack-allocate instead? char iohelper[] = SCRATCHDIRTEMPLATE;
+ + return ret==0 ? EXIT_SUCCESS : EXIT_FAILURE;
Spacing around == -- Eric Blake eblake redhat com +1-919-301-3266 Libvirt virtualization library http://libvirt.org
participants (2)
-
Daniel P. Berrange
-
Eric Blake