/*	$NetBSD: t_fcntl.c,v 1.6 2025/12/10 03:08:52 perseant Exp $	*/

#include <sys/types.h>
#include <sys/mount.h>
#include <sys/wait.h>

#include <atf-c.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <string.h>

#include <rump/rump.h>
#include <rump/rump_syscalls.h>

#include <ufs/ufs/ufsmount.h>
#include <ufs/lfs/lfs.h>
#include <ufs/lfs/lfs_extern.h>

#include "h_macros.h"
#include "util.h"

/* Debugging conditions */
/* #define FORCE_SUCCESS */ /* Don't actually revert, everything worked */
/* #define USE_DUMPLFS */ /* Dump the filesystem at certain steps */

#define UNCHANGED_CONTROL MP "/3-a-random-file"
#define FSSIZE 10000     /* In sectors */
#define A_FEW_BLOCKS 6500  /* In bytes; a few blocks worth */
#define MORE_THAN_A_SEGMENT (4 * SEGSIZE) /* In bytes; > SEGSIZE */

/* Set up filesystem with a file in it */
int setup(int, struct ufs_args *, off_t);

/* Actually run the test */
void coalesce(int);
void cleanseg(int);
void autoclean(int);

/* Unmount and check fsck */
void teardown(int, struct ufs_args *, off_t);

ATF_TC(coalesce32);
ATF_TC_HEAD(coalesce32, tc)
{
	atf_tc_set_md_var(tc, "descr",
		"LFS32 LFCNSCRAMBLE/LFCNREWRITEFILE");
	/* atf_tc_set_md_var(tc, "timeout", "20"); */
}

ATF_TC(coalesce64);
ATF_TC_HEAD(coalesce64, tc)
{
	atf_tc_set_md_var(tc, "descr",
		"LFS64 LFCNSCRAMBLE/LFCNREWRITEFILE");
	/* atf_tc_set_md_var(tc, "timeout", "20"); */
}

ATF_TC(cleanseg32);
ATF_TC_HEAD(cleanseg32, tc)
{
	atf_tc_set_md_var(tc, "descr",
		"LFS32 LFCNREWRITESEG");
	atf_tc_set_md_var(tc, "timeout", "20");
}

ATF_TC(cleanseg64);
ATF_TC_HEAD(cleanseg64, tc)
{
	atf_tc_set_md_var(tc, "descr",
		"LFS64 LFCNREWRITESEG");
	atf_tc_set_md_var(tc, "timeout", "20");
}

ATF_TC(autoclean32);
ATF_TC_HEAD(autoclean32, tc)
{
	atf_tc_set_md_var(tc, "descr",
		"LFS32 LFCNAUTOCLEAN");
	/* atf_tc_set_md_var(tc, "timeout", "20"); */
}

ATF_TC(autoclean64);
ATF_TC_HEAD(autoclean64, tc)
{
	atf_tc_set_md_var(tc, "descr",
		"LFS64 LFCNAUTOCLEAN");
	/* atf_tc_set_md_var(tc, "timeout", "20"); */
}

ATF_TC_BODY(coalesce32, tc)
{
	coalesce(32);
}

ATF_TC_BODY(coalesce64, tc)
{
	coalesce(64);
}

ATF_TC_BODY(cleanseg32, tc)
{
	cleanseg(32);
}

ATF_TC_BODY(cleanseg64, tc)
{
	cleanseg(64);
}

ATF_TC_BODY(autoclean32, tc)
{
	autoclean(32);
}

ATF_TC_BODY(autoclean64, tc)
{
	autoclean(64);
}

int setup(int width, struct ufs_args *argsp, off_t filesize)
{
	int fd;

	setvbuf(stdout, NULL, _IONBF, 0);

	/*
	 * Initialize.
	 */

	/* Create image file */
	create_lfs(FSSIZE, FSSIZE, width, 1);

	/* Mount filesystem */
	fprintf(stderr, "* Mount fs [1]\n");
	memset(argsp, 0, sizeof *argsp);
	argsp->fspec = __UNCONST(FAKEBLK);
	if (rump_sys_mount(MOUNT_LFS, MP, 0, argsp, sizeof *argsp) == -1)
		atf_tc_fail_errno("rump_sys_mount failed");

	/* Payload */
	fprintf(stderr, "* Initial payload\n");
	write_file(UNCHANGED_CONTROL, filesize, 1, 3);

	/* Make the data go to disk */
	fprintf(stderr, "* Double sync\n");
	rump_sys_sync();
	rump_sys_sync();

	/* Get a handle to the root of the file system */
	fd = rump_sys_open(MP, O_RDONLY);
	if (fd < 0)
		atf_tc_fail_errno("rump_sys_open mount point root failed");

	return fd;
}

void teardown(int fd, struct ufs_args *argsp, off_t filesize)
{
	/* Close descriptor */
	rump_sys_close(fd);

	/* Unmount */
	if (rump_sys_unmount(MP, 0) < 0)
		atf_tc_fail_errno("rump_sys_unmount failed[1]");

	/* Fsck */
	fprintf(stderr, "* Fsck after final unmount\n");
	if (fsck())
		atf_tc_fail("fsck found errors after final unmount");

	/*
	 * Check file system contents
	 */

	/* Mount filesystem one last time */
	fprintf(stderr, "* Mount fs again to check contents\n");
	if (rump_sys_mount(MOUNT_LFS, MP, 0, argsp, sizeof *argsp) == -1)
		atf_tc_fail_errno("rump_sys_mount failed [4]");

	if (check_file(UNCHANGED_CONTROL, filesize, 3) != 0)
		atf_tc_fail("Unchanged control file differs(!)");

	/* Umount filesystem */
	if (rump_sys_unmount(MP, 0) < 0)
		atf_tc_fail_errno("rump_sys_unmount failed[2]");

	/* Final fsck to double check */
	fprintf(stderr, "* Fsck after final unmount\n");
	if (fsck())
		atf_tc_fail("fsck found errors after final unmount");
}

void coalesce(int width)
{
	struct ufs_args args;
	struct stat statbuf;
	struct lfs_filestat_req fsr;
	struct lfs_filestats fss_before, fss_scrambled, fss_after;
	struct lfs_inode_array inotbl;
	int fd;
	ino_t ino;
		
	fd = setup(width, &args, A_FEW_BLOCKS);
	
	/* Get our file's inode number */
	if (rump_sys_stat(UNCHANGED_CONTROL, &statbuf) != 0)
		atf_tc_fail_errno("rump_sys_stat failed");
	ino = statbuf.st_ino;

	fprintf(stderr, "Treating inode %d\n", (int)ino);

	/* Retrieve fragmentation statistics */
	memset(&fss_before, 0, sizeof fss_before);
	memset(&fss_scrambled, 0, sizeof fss_scrambled);
	memset(&fss_after, 0, sizeof fss_after);
	
	fsr.ino = ino;
	fsr.len = 1;
	fsr.fss = &fss_before;
	if (rump_sys_fcntl(fd, LFCNFILESTATS, &fsr) < 0)
		atf_tc_fail_errno("LFCNFILESTATS[1] failed");

	inotbl.len = 1;
	inotbl.inodes = &ino;
	
	fprintf(stderr, "Start ino %d nblk %d count %d total %d\n",
		(int)ino, (int)fss_before.nblk, (int)fss_before.dc_count,
		(int)fss_before.dc_sum);

	/* Scramble */
	if (rump_sys_fcntl(fd, LFCNSCRAMBLE, &inotbl) < 0)
		atf_tc_fail_errno("LFCNSCRAMBLE failed");
	fsr.fss = &fss_scrambled;
	if (rump_sys_fcntl(fd, LFCNFILESTATS, &fsr) < 0)
		atf_tc_fail_errno("LFCNFILESTATS[2] failed");
	if (fss_scrambled.dc_count <= fss_before.dc_count)
		atf_tc_fail("Scramble did not worsen gap count");
	if (fss_scrambled.dc_sum <= fss_before.dc_sum)
		atf_tc_fail("Scramble did not worsen total gap length");
	
	fprintf(stderr, "Scrambled ino %d nblk %d count %d total %d\n",
		(int)ino, (int)fss_scrambled.nblk, (int)fss_scrambled.dc_count,
		(int)fss_scrambled.dc_sum);

	/* Coalesce */
	if (rump_sys_fcntl(fd, LFCNREWRITEFILE, &inotbl) < 0)
		atf_tc_fail_errno("LFCNREWRITEFILE failed");
	fsr.fss = &fss_after;
	if (rump_sys_fcntl(fd, LFCNFILESTATS, &fsr) < 0)
		atf_tc_fail_errno("LFCNFILESTATS[3] failed");
	if (fss_scrambled.dc_count <= fss_after.dc_count)
		atf_tc_fail("Rewrite did not improve gap count");
	if (fss_scrambled.dc_sum <= fss_after.dc_sum)
		atf_tc_fail("Rewrite did not improve total gap length");

	fprintf(stderr, "Coalesced ino %d nblk %d count %d total %d\n",
		(int)ino, (int)fss_after.nblk, (int)fss_after.dc_count,
		(int)fss_after.dc_sum);

	teardown(fd, &args, A_FEW_BLOCKS);
}

void cleanseg(int width)
{
	struct ufs_args args;
	struct lfs_segnum_array sna;
	struct lfs_seguse_array sua;
	int fd, sn;
		
	fd = setup(width, &args, MORE_THAN_A_SEGMENT);

	fprintf(stderr, "* Get seguse\n");
	sua.len = LFS_SEGUSE_MAXCNT;
	sua.start = 0;
	sua.seguse = malloc(LFS_SEGUSE_MAXCNT * sizeof(*sua.seguse));
	if (rump_sys_fcntl(fd, LFCNSEGUSE, &sua) < 0)
		atf_tc_fail_errno("LFCNSEGUSE[1] failed");

	for (sn = 0; sn < (int)sua.len; ++sn) {
		if (sua.seguse[sn].su_nbytes == 0)
			continue;
		if ((sua.seguse[sn].su_flags & (SEGUSE_DIRTY | SEGUSE_ACTIVE))
		    != SEGUSE_DIRTY)
			continue;
		break;
	}
	if (sn == (int)sua.len)
		atf_tc_fail("No segments found to clean");
	
	fprintf(stderr, "* Cleaning segment #%d\n", sn);

	memset(&sna, 0, sizeof sna);
	sna.len = 1;
	sna.segments = &sn;
	if (rump_sys_fcntl(fd, LFCNREWRITESEGS, &sna) < 0)
		atf_tc_fail_errno("LFCNREWRITESEGS failed");

	fprintf(stderr, "* Double sync\n");
	rump_sys_sync();
	rump_sys_sync();

	fprintf(stderr, "* Get seguse again\n");
	sua.len = 1;
	sua.start = sn;
	if (rump_sys_fcntl(fd, LFCNSEGUSE, &sua) < 0)
		atf_tc_fail_errno("LFCNSEGUSE[2] failed");

	if (sua.seguse[0].su_nbytes > 0)
		atf_tc_fail("Empty bytes after clean");

	fprintf(stderr, "* Teardown\n");
	teardown(fd, &args, MORE_THAN_A_SEGMENT);
}

void autoclean(int width)
{
	struct ufs_args args;
	struct lfs_autoclean_params autoclean;
	int i, fd;
	char filename[1024];
	struct statvfs statbuf;
	CLEANERINFO64 ci_before, ci_after;
	unsigned long target;
		
	fd = setup(width, &args, CHUNKSIZE);

	rump_sys_fstatvfs1(fd, &statbuf, ST_WAIT);
	target = 7 * statbuf.f_blocks / 8;

	/* Write a number of files, deleting every other one. */
	fprintf(stderr, "* Write numbered files\n");
	for (i = 4; ; i++) {
		fprintf(stderr, "* create %d\n", i);
		sprintf(filename, "%s/%d", MP, i);
		write_file(filename, SEGSIZE / 3, 1, 0);
		rump_sys_fstatvfs1(fd, &statbuf, ST_WAIT);
		if (statbuf.f_bfree < target)
			break;
		fprintf(stderr, "  %ld blocks vs target %ld\n",
			(long)statbuf.f_bfree, (long)target);
	}
	rump_sys_sync();
	rump_sys_sync();

	/* Record cleanerinfo */
	fprintf(stderr, "* Get cleanerinfo\n");
	if (rump_sys_fcntl(fd, LFCNCLEANERINFO, &ci_before) < 0)
		atf_tc_fail_errno("LFCNCLEANERINFO[1] failed");

	/* Start autocleaner */
	autoclean.size = sizeof(autoclean);
	autoclean.mode = LFS_CLEANMODE_GREEDY;
	autoclean.thresh = -1;
	autoclean.target = -1;
	if (rump_sys_fcntl(fd, LFCNAUTOCLEAN, &autoclean) < 0)
		atf_tc_fail_errno("LFCNAUTOCLEAN[1] failed");

	/* Make many segments almost empty */
	fprintf(stderr, "* Delete most files\n");
	for (; i > 0; i--) {
		if (i % 3 == 0)
			continue;
		fprintf(stderr, "* delete %d\n", i);
		sprintf(filename, "%s/%d", MP, i);
		rump_sys_unlink(filename);
	}
	rump_sys_sync();
	rump_sys_sync();

	/* Give the cleaner a chance to run */
	fprintf(stderr, "* Sleep\n");
	sleep(5);

	/* Auto reclaim freed segments */
	fprintf(stderr, "* Sync\n");
	rump_sys_sync();
	rump_sys_sync();
	
	/* Record cleanerinfo again */
	fprintf(stderr, "* Get cleanerinfo again\n");
	if (rump_sys_fcntl(fd, LFCNCLEANERINFO, &ci_after) < 0)
		atf_tc_fail_errno("LFCNCLEANERINFO[2] failed");

	/* Compare */
	if (ci_before.avail >= ci_after.avail)
		atf_tc_fail("No improvement");
	
	teardown(fd, &args, CHUNKSIZE);
}

ATF_TP_ADD_TCS(tp)
{

	ATF_TP_ADD_TC(tp, coalesce32);
	ATF_TP_ADD_TC(tp, coalesce64);
	ATF_TP_ADD_TC(tp, cleanseg32);
	ATF_TP_ADD_TC(tp, cleanseg64);
	ATF_TP_ADD_TC(tp, autoclean32);
	ATF_TP_ADD_TC(tp, autoclean64);
	return atf_no_error();
}
