[1/1] Use real FS block size in fallocate05
diff mbox series

Message ID 20191128093610.6903-2-mdoucha@suse.cz
State Changes Requested
Delegated to: Petr Vorel
Headers show
Series
  • Use real FS block size in fallocate05
Related show

Commit Message

Martin Doucha Nov. 28, 2019, 9:36 a.m. UTC
fallocate() behavior depends on whether the file range is aligned to full
blocks. Make sure that the test always uses aligned file range so that
the test is consistent across platforms.

Also use the TEST() macro to prevent errno pollution and add notes about
some file system quirks that may or may not be bugs.

Signed-off-by: Martin Doucha <mdoucha@suse.cz>
---
 .../kernel/syscalls/fallocate/fallocate05.c   | 75 ++++++++++++-------
 1 file changed, 50 insertions(+), 25 deletions(-)

Comments

Petr Vorel Nov. 28, 2019, 5:47 p.m. UTC | #1
Hi Martin,

Looks ok to me.

BTW there is change on results on some of my VM:

Old version:
tst_test.c:1169: INFO: Testing on ext4
tst_mkfs.c:89: INFO: Formatting /dev/loop0 with ext4 opts='' extra opts=''
mke2fs 1.43.8 (1-Jan-2018)
tst_test.c:1106: INFO: Timeout per run is 0h 05m 00s
tst_fill_fs.c:29: INFO: Creating file mntpoint/file0 size 21710183
tst_fill_fs.c:29: INFO: Creating file mntpoint/file1 size 8070086
tst_fill_fs.c:29: INFO: Creating file mntpoint/file2 size 3971177
tst_fill_fs.c:29: INFO: Creating file mntpoint/file3 size 36915315
tst_fill_fs.c:29: INFO: Creating file mntpoint/file4 size 70310993
tst_fill_fs.c:29: INFO: Creating file mntpoint/file5 size 4807935
tst_fill_fs.c:29: INFO: Creating file mntpoint/file6 size 90739786
tst_fill_fs.c:29: INFO: Creating file mntpoint/file7 size 76896492
tst_fill_fs.c:49: INFO: write(): ENOSPC
fallocate05.c:50: PASS: write() wrote 8192 bytes
fallocate05.c:59: PASS: fallocate() on full FS: ENOSPC
fallocate05.c:68: PASS: fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE)
fallocate05.c:74: PASS: write()

With your patch:
...
tst_fill_fs.c:49: INFO: write(): ENOSPC (28)
fallocate05.c:67: PASS: write() wrote 16384 bytes
fallocate05.c:73: FAIL: fallocate() succeeded unexpectedly

Maybe it's correct (previous version didn't catch a problem),
not really sure.

+ there are few simple warnings:
../../../../include/tst_test.h:70:41: warning: format ‘%i’ expects argument of type ‘int’, but argument 5 has type ‘size_t’ {aka ‘long unsigned int’} [-Wformat=]
   70 |   tst_brk_(__FILE__, __LINE__, (ttype), (arg_fmt), ##__VA_ARGS__);\
      |                                         ^~~~~~~~~
fallocate05.c:53:3: note: in expansion of macro ‘tst_brk’
   53 |   tst_brk(TBROK | TTERRNO, "fallocate(fd, 0, 0, %i)", bufsize);
      |   ^~~~~~~
fallocate05.c:62:19: warning: comparison of integer expressions of different signedness: ‘long int’ and ‘size_t’ {aka ‘long unsigned int’} [-Wsign-compare]
   62 |  else if (TST_RET != bufsize)
      |                   ^~
fallocate05.c:27:18: warning: unused variable ‘i’ [-Wunused-variable]
   27 |  size_t bufsize, i;
      |                  ^

Kind regards,
Petr
Martin Doucha Nov. 29, 2019, 9:54 a.m. UTC | #2
On 11/28/19 6:47 PM, Petr Vorel wrote:
> Hi Martin,
> 
> Looks ok to me.
> 
> BTW there is change on results on some of my VM:
> 
> Old version:
> tst_test.c:1169: INFO: Testing on ext4
> tst_mkfs.c:89: INFO: Formatting /dev/loop0 with ext4 opts='' extra opts=''
> mke2fs 1.43.8 (1-Jan-2018)
> tst_test.c:1106: INFO: Timeout per run is 0h 05m 00s
> tst_fill_fs.c:29: INFO: Creating file mntpoint/file0 size 21710183
> tst_fill_fs.c:29: INFO: Creating file mntpoint/file1 size 8070086
> tst_fill_fs.c:29: INFO: Creating file mntpoint/file2 size 3971177
> tst_fill_fs.c:29: INFO: Creating file mntpoint/file3 size 36915315
> tst_fill_fs.c:29: INFO: Creating file mntpoint/file4 size 70310993
> tst_fill_fs.c:29: INFO: Creating file mntpoint/file5 size 4807935
> tst_fill_fs.c:29: INFO: Creating file mntpoint/file6 size 90739786
> tst_fill_fs.c:29: INFO: Creating file mntpoint/file7 size 76896492
> tst_fill_fs.c:49: INFO: write(): ENOSPC
> fallocate05.c:50: PASS: write() wrote 8192 bytes
> fallocate05.c:59: PASS: fallocate() on full FS: ENOSPC
> fallocate05.c:68: PASS: fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE)
> fallocate05.c:74: PASS: write()
> 
> With your patch:
> ...
> tst_fill_fs.c:49: INFO: write(): ENOSPC (28)
> fallocate05.c:67: PASS: write() wrote 16384 bytes
> fallocate05.c:73: FAIL: fallocate() succeeded unexpectedly
> 
> Maybe it's correct (previous version didn't catch a problem),
> not really sure.

Your VM seems to use 1KB blocks in the ext4 test partition. It's
probably the same quirk as I've documented for XFS in the patch. I'd
like some more opinions on how to deal with it:
- improve tst_fill_fs() and make sure that fallocate(1 block) always
fails after it?
- set some reasonable minimum size where fallocate() must fail on full
FS but allow fallocate() to pass with smaller allocation requests?
- drop that one fallocate() fail check entirely because it's too unreliable?

> + there are few simple warnings:

I'll resubmit after we agree on a solution to the above problem.
Jan Stancek Nov. 29, 2019, 12:01 p.m. UTC | #3
Hi,

<snip>

>  
>  static void run(void)
>  {
> -	char buf[FALLOCATE_SIZE];
> -	ssize_t ret;
> +	size_t bufsize, i;
> +	struct stat statbuf;
>  
>  	fd = SAFE_OPEN(MNTPOINT "/test_file", O_WRONLY | O_CREAT);
>  
> -	if (fallocate(fd, 0, 0, FALLOCATE_SIZE)) {
> -		if (errno == EOPNOTSUPP) {
> -			tst_res(TCONF | TERRNO, "fallocate() not supported");
> +	// Use real FS block size, otherwise fallocate() call will test
> +	// different things on different platforms

Style guide favors c-style comments.

> +	SAFE_FSTAT(fd, &statbuf);
> +	bufsize = FALLOCATE_BLOCKS * statbuf.st_blksize;
> +	buf = realloc(buf, bufsize);
> +
> +	if (!buf) {
> +		tst_brk(TBROK, "Buffer allocation failed");
> +		SAFE_CLOSE(fd);
> +		return;

Anything after TBROK will be unreachable.

> +	}
> +
> +	TEST(fallocate(fd, 0, 0, bufsize));
> +
> +	if (TST_RET) {
> +		if (errno == ENOTSUP) {

TEST_ERR

> +			tst_res(TCONF | TTERRNO, "fallocate() not supported");

tst_brk would make more sense here. If we fail here we can end the test.

>  			SAFE_CLOSE(fd);
>  			return;
>  		}
>  
> -		tst_brk(TBROK | TERRNO,
> -			"fallocate(fd, 0, 0, %i)", FALLOCATE_SIZE);
> +		tst_brk(TBROK | TTERRNO, "fallocate(fd, 0, 0, %i)", bufsize);
>  	}
>  
>  	tst_fill_fs(MNTPOINT, 1);
>  
> -	ret = write(fd, buf, sizeof(buf));
> +	TEST(write(fd, buf, bufsize));
>  
> -	if (ret < 0)
> -		tst_res(TFAIL | TERRNO, "write() failed unexpectedly");
> +	if (TST_RET < 0)
> +		tst_res(TFAIL | TTERRNO, "write() failed unexpectedly");
> +	else if (TST_RET != bufsize)
> +		tst_res(TFAIL,
> +			"Short write(): %ld bytes (expected %zu)",
> +			TST_RET, bufsize);
>  	else
> -		tst_res(TPASS, "write() wrote %zu bytes", ret);
> +		tst_res(TPASS, "write() wrote %ld bytes", TST_RET);
>  
> -	ret = fallocate(fd, 0, FALLOCATE_SIZE, FALLOCATE_SIZE);
> -	if (ret != -1)
> +	// fallocate(1 block) may pass here on XFS. Original test allocated
> +	// 8KB (2 blocks on x86) so keep the original behavior.
> +	TEST(fallocate(fd, 0, bufsize, 2 * statbuf.st_blksize));

I don't understand why there is need to find minimum value that can
satisfy this check. It looks like we are testing tst_fill_fs() more
than fallocate().

In other words, what is wrong with current test? Is the problem that
FALLOCATE_SIZE (1M) is not aligned on all platforms? Or is the test
invalid with FALLOCATE_SIZE that big? Or both?

<snip>

>  
> @@ -80,6 +102,9 @@ static void cleanup(void)
>  {
>  	if (fd > 0)
>  		SAFE_CLOSE(fd);
> +
> +	if (buf)

Check is not needed, free() can handle NULL.
Martin Doucha Nov. 29, 2019, 3:25 p.m. UTC | #4
On 11/29/19 1:01 PM, Jan Stancek wrote:
>> +			tst_res(TCONF | TTERRNO, "fallocate() not supported");
> 
> tst_brk would make more sense here. If we fail here we can end the test.

No. tst_brk() will terminate the whole test on the first usual test case
(Ext2) and skip all the other file systems that do support fallocate().

> I don't understand why there is need to find minimum value that can
> satisfy this check. It looks like we are testing tst_fill_fs() more
> than fallocate().
> 
> In other words, what is wrong with current test? Is the problem that
> FALLOCATE_SIZE (1M) is not aligned on all platforms? Or is the test
> invalid with FALLOCATE_SIZE that big? Or both?

I don't like to blindly rely on the assumption that block size is always
a power of 2 and smaller than some magic number. Getting the real block
size is trivial. The only real question is how many free blocks do we
allow on a "full" file system in our tests. 1MB is just 16 blocks on
PPC64 so the magic number isn't particularly big anyway.

I've implemented the rest of your suggestions and I'll resubmit later
next week.
Jan Stancek Nov. 29, 2019, 4:17 p.m. UTC | #5
----- Original Message -----
> On 11/29/19 1:01 PM, Jan Stancek wrote:
> >> +			tst_res(TCONF | TTERRNO, "fallocate() not supported");
> > 
> > tst_brk would make more sense here. If we fail here we can end the test.
> 
> No. tst_brk() will terminate the whole test on the first usual test case
> (Ext2) and skip all the other file systems that do support fallocate().

It shouldn't. tst_brk() does call exit() for test process, but
.all_filesystems spawns new process for each fs.

If I add:
   tst_brk(TCONF, "STOP");
to run(), I get STOP for each fs:
  $ sudo ./fallocate05 2>&1 | grep STOP
  fallocate05.c:30: CONF: STOP
  fallocate05.c:30: CONF: STOP
  fallocate05.c:30: CONF: STOP
  fallocate05.c:30: CONF: STOP
  fallocate05.c:30: CONF: STOP
  fallocate05.c:30: CONF: STOP

> 
> > I don't understand why there is need to find minimum value that can
> > satisfy this check. It looks like we are testing tst_fill_fs() more
> > than fallocate().
> > 
> > In other words, what is wrong with current test? Is the problem that
> > FALLOCATE_SIZE (1M) is not aligned on all platforms? Or is the test
> > invalid with FALLOCATE_SIZE that big? Or both?
> 
> I don't like to blindly rely on the assumption that block size is always
> a power of 2 and smaller than some magic number. Getting the real block
> size is trivial. The only real question is how many free blocks do we
> allow on a "full" file system in our tests. 1MB is just 16 blocks on
> PPC64 so the magic number isn't particularly big anyway.

OK, let's assume 16 is enough. Can we use that value also for ENOSPC check?
  TEST(fallocate(fd, 0, bufsize, 2 * statbuf.st_blksize));
Martin Doucha Dec. 4, 2019, 10:38 a.m. UTC | #6
On 11/29/19 5:17 PM, Jan Stancek wrote:
>> No. tst_brk() will terminate the whole test on the first usual test case
>> (Ext2) and skip all the other file systems that do support fallocate().
> 
> It shouldn't. tst_brk() does call exit() for test process, but
> .all_filesystems spawns new process for each fs.

You're right, I'll use tst_brk(TCONF, ...) then.

>> I don't like to blindly rely on the assumption that block size is always
>> a power of 2 and smaller than some magic number. Getting the real block
>> size is trivial. The only real question is how many free blocks do we
>> allow on a "full" file system in our tests. 1MB is just 16 blocks on
>> PPC64 so the magic number isn't particularly big anyway.
> 
> OK, let's assume 16 is enough. Can we use that value also for ENOSPC check?
>   TEST(fallocate(fd, 0, bufsize, 2 * statbuf.st_blksize));

I think it might be better to change the test scenario a bit:
1. fallocate(FALLOCATE_BLOCKS * blocksize)
2. tst_fill_fs()
3. write(FALLOCATE_BLOCKS * blocksize)
4. repeat fallocate(blocksize) until we get ENOSPC
5. write() into all blocks allocated in step 4
6. check that another write() will get ENOSPC
7. test fallocate(PUNCH_HOLE | KEEP_SIZE)

This should get us around the issue with tst_fill_fs() and still
properly validate that fallocate() handles full FS gracefully.

The only remaining issue is whether it's correct for Btrfs to only
release blocks when you deallocate the whole file. I still haven't heard
back from our Btrfs dev.
Cyril Hrubis Dec. 13, 2019, 1:40 p.m. UTC | #7
Hi!
> I think it might be better to change the test scenario a bit:
> 1. fallocate(FALLOCATE_BLOCKS * blocksize)
> 2. tst_fill_fs()
> 3. write(FALLOCATE_BLOCKS * blocksize)
> 4. repeat fallocate(blocksize) until we get ENOSPC
> 5. write() into all blocks allocated in step 4
> 6. check that another write() will get ENOSPC
> 7. test fallocate(PUNCH_HOLE | KEEP_SIZE)
> 
> This should get us around the issue with tst_fill_fs() and still
> properly validate that fallocate() handles full FS gracefully.

Looping over the second fallocate until we got ENOSPC sounds reasonable
to me.

> The only remaining issue is whether it's correct for Btrfs to only
> release blocks when you deallocate the whole file. I still haven't heard
> back from our Btrfs dev.

So the punched hole does not free space on Btrfs even if we are FS block
aligned? I was under an impression that it should, but again Btrfs is
copy-on-write filesystem, so it may want to keep a copy of the discarded
blocks anyways.

Patch
diff mbox series

diff --git a/testcases/kernel/syscalls/fallocate/fallocate05.c b/testcases/kernel/syscalls/fallocate/fallocate05.c
index 17034e5b1..040f176b9 100644
--- a/testcases/kernel/syscalls/fallocate/fallocate05.c
+++ b/testcases/kernel/syscalls/fallocate/fallocate05.c
@@ -11,65 +11,87 @@ 
 
 #include <stdio.h>
 #include <stdlib.h>
-#include <errno.h>
 #include <fcntl.h>
 #include "tst_test.h"
 #include "lapi/fallocate.h"
 
 #define MNTPOINT "mntpoint"
-#define FALLOCATE_SIZE (1024*1024)
+#define FALLOCATE_BLOCKS 16
 #define TESTED_FLAGS "fallocate(FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE)"
 
 static int fd;
+static char *buf = NULL;
 
 static void run(void)
 {
-	char buf[FALLOCATE_SIZE];
-	ssize_t ret;
+	size_t bufsize, i;
+	struct stat statbuf;
 
 	fd = SAFE_OPEN(MNTPOINT "/test_file", O_WRONLY | O_CREAT);
 
-	if (fallocate(fd, 0, 0, FALLOCATE_SIZE)) {
-		if (errno == EOPNOTSUPP) {
-			tst_res(TCONF | TERRNO, "fallocate() not supported");
+	// Use real FS block size, otherwise fallocate() call will test
+	// different things on different platforms
+	SAFE_FSTAT(fd, &statbuf);
+	bufsize = FALLOCATE_BLOCKS * statbuf.st_blksize;
+	buf = realloc(buf, bufsize);
+
+	if (!buf) {
+		tst_brk(TBROK, "Buffer allocation failed");
+		SAFE_CLOSE(fd);
+		return;
+	}
+
+	TEST(fallocate(fd, 0, 0, bufsize));
+
+	if (TST_RET) {
+		if (errno == ENOTSUP) {
+			tst_res(TCONF | TTERRNO, "fallocate() not supported");
 			SAFE_CLOSE(fd);
 			return;
 		}
 
-		tst_brk(TBROK | TERRNO,
-			"fallocate(fd, 0, 0, %i)", FALLOCATE_SIZE);
+		tst_brk(TBROK | TTERRNO, "fallocate(fd, 0, 0, %i)", bufsize);
 	}
 
 	tst_fill_fs(MNTPOINT, 1);
 
-	ret = write(fd, buf, sizeof(buf));
+	TEST(write(fd, buf, bufsize));
 
-	if (ret < 0)
-		tst_res(TFAIL | TERRNO, "write() failed unexpectedly");
+	if (TST_RET < 0)
+		tst_res(TFAIL | TTERRNO, "write() failed unexpectedly");
+	else if (TST_RET != bufsize)
+		tst_res(TFAIL,
+			"Short write(): %ld bytes (expected %zu)",
+			TST_RET, bufsize);
 	else
-		tst_res(TPASS, "write() wrote %zu bytes", ret);
+		tst_res(TPASS, "write() wrote %ld bytes", TST_RET);
 
-	ret = fallocate(fd, 0, FALLOCATE_SIZE, FALLOCATE_SIZE);
-	if (ret != -1)
+	// fallocate(1 block) may pass here on XFS. Original test allocated
+	// 8KB (2 blocks on x86) so keep the original behavior.
+	TEST(fallocate(fd, 0, bufsize, 2 * statbuf.st_blksize));
+	if (TST_RET != -1)
 		tst_brk(TFAIL, "fallocate() succeeded unexpectedly");
 
-	if (errno != ENOSPC)
-		tst_brk(TFAIL | TERRNO, "fallocate() should fail with ENOSPC");
+	if (TST_ERR != ENOSPC)
+		tst_brk(TFAIL | TTERRNO, "fallocate() should fail with ENOSPC");
 
-	tst_res(TPASS | TERRNO, "fallocate() on full FS");
+	tst_res(TPASS | TTERRNO, "fallocate() on full FS");
 
-	ret = fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0, FALLOCATE_SIZE);
-	if (ret == -1) {
-		if (errno == EOPNOTSUPP)
+	// btrfs won't release any space unless the whole file has been
+	// deallocated. Original test did that, keep the original behavior.
+	TEST(fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0,
+		bufsize));
+	if (TST_RET == -1) {
+		if (TST_ERR == ENOTSUP)
 			tst_brk(TCONF, TESTED_FLAGS);
 
-		tst_brk(TBROK | TERRNO, TESTED_FLAGS);
+		tst_brk(TBROK | TTERRNO, TESTED_FLAGS);
 	}
 	tst_res(TPASS, TESTED_FLAGS);
 
-	ret = write(fd, buf, 10);
-	if (ret == -1)
-		tst_res(TFAIL | TERRNO, "write()");
+	TEST(write(fd, buf, 10));
+	if (TST_RET == -1)
+		tst_res(TFAIL | TTERRNO, "write()");
 	else
 		tst_res(TPASS, "write()");
 
@@ -80,6 +102,9 @@  static void cleanup(void)
 {
 	if (fd > 0)
 		SAFE_CLOSE(fd);
+
+	if (buf)
+		free(buf);
 }
 
 static struct tst_test test = {