[glibc] libio: Add a new fwrite test that evaluates partial writes
Tulio Magno Quites Machado Filho
tuliom@sourceware.org
Tue Jan 28 19:04:46 GMT 2025
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=1515f74fd81035a79861cd9fa12053fa9450ec65
commit 1515f74fd81035a79861cd9fa12053fa9450ec65
Author: Tulio Magno Quites Machado Filho <tuliom@redhat.com>
Date: Tue Jan 28 15:37:44 2025 -0300
libio: Add a new fwrite test that evaluates partial writes
Test if the file-position is correctly updated when fwrite tries to
flush its internal cache but is not able to completely write all items.
Reviewed-by: DJ Delorie <dj@redhat.com>
Diff:
---
stdio-common/Makefile | 1 +
stdio-common/tst-fwrite-pos.c | 233 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 234 insertions(+)
diff --git a/stdio-common/Makefile b/stdio-common/Makefile
index 0b9f85e129..1e7bca641e 100644
--- a/stdio-common/Makefile
+++ b/stdio-common/Makefile
@@ -264,6 +264,7 @@ tests := \
tst-fwrite-memstrm \
tst-fwrite-overflow \
tst-fwrite-pipe \
+ tst-fwrite-pos \
tst-fwrite-ro \
tst-getline \
tst-getline-enomem \
diff --git a/stdio-common/tst-fwrite-pos.c b/stdio-common/tst-fwrite-pos.c
new file mode 100644
index 0000000000..3923490d59
--- /dev/null
+++ b/stdio-common/tst-fwrite-pos.c
@@ -0,0 +1,233 @@
+/* Test if fwrite returns consistent values on partial writes.
+ Copyright (C) 2025 Free Software Foundation, Inc.
+ This file is part of the GNU C Library.
+
+ The GNU C 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.
+
+ The GNU C 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 the GNU C Library; if not, see
+ <https://www.gnu.org/licenses/>. */
+
+#include <errno.h>
+/* stdio.h provides BUFSIZ, which is the size of fwrite's internal buffer. */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <support/check.h>
+#include <support/fuse.h>
+#include <support/support.h>
+#include <support/xstdio.h>
+#include <support/temp_file.h>
+
+/* Length of the buffer in bytes. */
+#define INBUF_SIZE (BUFSIZ)
+
+/* Amount of bytes written to fwrite's internal cache that trigger a
+ flush. */
+#define CACHE_THRESHOLD (BUFSIZ / 2)
+
+#define ITERATIONS 1000
+
+/* Maximum number of bytes written during a partial write. */
+#define PARTIAL_BYTES 4
+
+#define EXPECT_EVENT(opcode, state, expected_state) \
+ { \
+ if (state != expected_state) \
+ { \
+ char *s = support_fuse_opcode (opcode); \
+ FAIL ("unexpected event %s at state %d", s, state); \
+ free (s); \
+ } \
+ }
+
+/* The goal of this test is to check that file position of a file stream is
+ correctly updated when write () returns a partial write.
+ The file system simulates pseudorandom partial writes while the test is
+ running.
+ Meanwhile the main thread calls fwrite () with a large object first and
+ small objects later. The usage of a large enough object ensures that
+ fwrite's internal cache is full enough, without triggering a write to file.
+ Subsequent calls to fwrite are guaranteed to trigger a write to file. */
+
+static void
+fuse_thread (struct support_fuse *f, void *closure)
+{
+ struct fuse_in_header *inh;
+ int state = 0;
+ while ((inh = support_fuse_next (f)) != NULL)
+ {
+ {
+ char *opcode = support_fuse_opcode (inh->opcode);
+ printf ("info: (T) event %s(%llu) len=%u nodeid=%llu\n",
+ opcode, (unsigned long long int) inh->unique, inh->len,
+ (unsigned long long int) inh->nodeid);
+ free (opcode);
+ }
+
+ /* Handle mountpoint and basic directory operation for the root (1). */
+ if (support_fuse_handle_mountpoint (f)
+ || (inh->nodeid == 1 && support_fuse_handle_directory (f)))
+ continue;
+
+ switch (inh->opcode)
+ {
+ case FUSE_LOOKUP:
+ EXPECT_EVENT (inh->nodeid, state, 0);
+ state++;
+ support_fuse_reply_error (f, ENOENT);
+ break;
+ case FUSE_CREATE:
+ EXPECT_EVENT (inh->nodeid, state, 1);
+ state++;
+ struct fuse_entry_out *entry;
+ struct fuse_open_out *open;
+ support_fuse_prepare_create (f, 2, &entry, &open);
+ entry->attr.mode = S_IFREG | 0600;
+ support_fuse_reply_prepared (f);
+ break;
+ case FUSE_GETXATTR:
+ /* We don't need to support extended attributes in this test. */
+ support_fuse_reply_error (f, ENOSYS);
+ break;
+ case FUSE_GETATTR:
+ /* Happens after open. */
+ if (inh->nodeid == 2)
+ {
+ struct fuse_attr_out *out = support_fuse_prepare_attr (f);
+ out->attr.mode = S_IFREG | 0600;
+ out->attr.size = 0;
+ support_fuse_reply_prepared (f);
+ }
+ else
+ support_fuse_reply_error (f, ENOENT);
+ break;
+ case FUSE_WRITE:
+ if (inh->nodeid == 2)
+ {
+ struct fuse_write_out out;
+ if (state > 1 && state < ITERATIONS + 2)
+ {
+ /* The 2nd and subsequent calls to fwrite () trigger a
+ flush of fwrite's internal cache. Simulate a partial
+ write of up to PARTIAL_BYTES bytes. */
+ out.padding = 0;
+ out.size = 1 + rand () % PARTIAL_BYTES,
+ state++;
+ support_fuse_reply (f, &out, sizeof (out));
+ }
+ else if (state >= ITERATIONS + 2)
+ {
+ /* This request is expected to come from fflush (). Copy
+ all the data successfully. This may be executed more
+ than once. */
+ struct fuse_write_in *p = support_fuse_cast (WRITE, inh);
+ out.padding = 0;
+ out.size = p->size,
+ state++;
+ support_fuse_reply (f, &out, sizeof (out));
+ }
+ else
+ support_fuse_reply_error (f, EIO);
+ }
+ else
+ support_fuse_reply_error (f, EIO);
+ break;
+ case FUSE_FLUSH:
+ case FUSE_RELEASE:
+ TEST_COMPARE (inh->nodeid, 2);
+ support_fuse_reply_empty (f);
+ break;
+ default:
+ FAIL ("unexpected event %s", support_fuse_opcode (inh->opcode));
+ support_fuse_reply_error (f, EIO);
+ }
+ }
+}
+
+static int
+do_test (void)
+{
+ char *in;
+ int i;
+ size_t written;
+
+ _Static_assert (CACHE_THRESHOLD <= INBUF_SIZE,
+ "the input buffer must be larger than the cache threshold");
+ /* Avoid filling up fwrite's cache. */
+ _Static_assert (CACHE_THRESHOLD - 1 + PARTIAL_BYTES * ITERATIONS <= BUFSIZ,
+ "fwrite's cache must fit all data written");
+
+ support_fuse_init ();
+ struct support_fuse *fs = support_fuse_mount (fuse_thread, NULL);
+
+ /* Create and open a temporary file in the fuse mount point. */
+ char *fname = xasprintf ("%s/%sXXXXXX", support_fuse_mountpoint (fs),
+ "tst-fwrite-fuse");
+ int fd = mkstemp (fname);
+ TEST_VERIFY_EXIT (fd != -1);
+ FILE *f = fdopen (fd, "w");
+ TEST_VERIFY_EXIT (f != NULL);
+
+ /* Allocate an input array that will be written to the temporary file. */
+ in = xmalloc (INBUF_SIZE);
+ for (i = 0; i < INBUF_SIZE; i++)
+ in[i] = i % 0xff;
+
+ /* Ensure the file position indicator is at the beginning of the stream. */
+ TEST_COMPARE (ftell (f), 0);
+
+ /* Try to fill as most data to the cache of the file stream as possible
+ with a single large object.
+ All data is expected to be written to the cache.
+ No errors are expected from this. */
+ TEST_COMPARE (fwrite (in, CACHE_THRESHOLD - 1, 1, f), 1);
+ TEST_COMPARE (ferror (f), 0);
+ written = CACHE_THRESHOLD - 1;
+
+ /* Ensure the file position indicator advanced correctly. */
+ TEST_COMPARE (ftell (f), written);
+
+ for (i = 0; i < ITERATIONS; i++)
+ {
+ /* Write an extra object of size PARTIAL_BYTES that triggers a write to
+ disk. Our FS will write at most PARTIAL_BYTES bytes to the file
+ instead of all the data. By writting PARTIAL_BYTES, we guarantee
+ the amount of data in the cache will never decrease below
+ CACHE_THRESHOLD.
+ No errors are expected. */
+ TEST_COMPARE (fwrite (in, PARTIAL_BYTES, 1, f), 1);
+ TEST_COMPARE (ferror (f), 0);
+ written += PARTIAL_BYTES;
+
+ /* Ensure the file position indicator advanced correctly. */
+ TEST_COMPARE (ftell (f), written);
+ }
+
+ /* Flush the rest of the data. */
+ TEST_COMPARE (fflush (f), 0);
+ TEST_COMPARE (ferror (f), 0);
+
+ /* Ensure the file position indicator was not modified. */
+ TEST_COMPARE (ftell (f), written);
+
+ /* In case an unexpected error happened, clear it before exiting. */
+ if (ferror (f))
+ clearerr (f);
+
+ xfclose (f);
+ free (fname);
+ free (in);
+ support_fuse_unmount (fs);
+ return 0;
+}
+
+#include <support/test-driver.c>
More information about the Glibc-cvs
mailing list