263 lines
8.6 KiB
Diff
263 lines
8.6 KiB
Diff
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 --git a/stdio-common/Makefile b/stdio-common/Makefile
|
|
index b4a1e62f4a388d0a..acf7059ba50d2ca9 100644
|
|
--- a/stdio-common/Makefile
|
|
+++ b/stdio-common/Makefile
|
|
@@ -220,6 +220,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 0000000000000000..3923490d5923b4b4
|
|
--- /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>
|