/*
 * Copyright (C) 2007 Nokia Corporation.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * version 2 as published by the Free Software Foundation.
 *
 * This program 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 *
 * Author: Adrian Hunter
 */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <errno.h>
#include <libgen.h>
#include <dirent.h>
#include <ctype.h>
#include <limits.h>
#include <mntent.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/vfs.h>
#include <sys/mount.h>
#include <sys/statvfs.h>
#include <linux/fs.h>
#include <linux/jffs2.h>

#include "tests.h"

const char *tests_file_system_mount_dir = TESTS_DEFAULT_FILE_SYSTEM_MOUNT_DIR;

const char *tests_file_system_type = TESTS_DEFAULT_FILE_SYSTEM_TYPE;

int tests_ok_to_sync = 0; /* Whether to use fsync */

/* General purpose test parameter to specify some aspect of test size.
   May be used by different tests in different ways or not at all.
   Set by the -z or --size option. */
int64_t tests_size_parameter = 0;

/* General purpose test parameter to specify some aspect of test repetition.
   May be used by different tests in different ways or not at all.
   Set by the -n, --repeat options. */
int64_t tests_repeat_parameter = 0;

/* General purpose test parameter to specify some aspect of test sleeping.
   May be used by different tests in different ways or not at all.
   Set by the -p, --sleep options. */
int64_t tests_sleep_parameter = 0;

/* Program name from argv[0] */
const char *program_name = "unknown";

/* General purpose test parameter to specify a file should be unlinked.
   May be used by different tests in different ways or not at all. */
int tests_unlink_flag = 0;

/* General purpose test parameter to specify a file should be closed.
   May be used by different tests in different ways or not at all. */
int tests_close_flag = 0;

/* General purpose test parameter to specify a file should be deleted.
   May be used by different tests in different ways or not at all. */
int tests_delete_flag = 0;

/* General purpose test parameter to specify a file have a hole.
   May be used by different tests in different ways or not at all. */
int tests_hole_flag = 0;

/* Whether it is ok to test on the root file system */
static int rootok = 0;

/* Maximum file name length of test file system (from statfs) */
long tests_max_fname_len = 255;

/* Function invoked by the CHECK macro */
void tests_test(int test,const char *msg,const char *file,unsigned line)
{
	int eno;
	time_t t;

	if (test)
		return;
	eno = errno;
	time(&t);
	fprintf(stderr,	"Test failed: %s on %s"
			"Test failed: %s in %s at line %u\n",
			program_name, ctime(&t), msg, file, line);
	if (eno) {
		fprintf(stderr,"errno = %d\n",eno);
		fprintf(stderr,"strerror = %s\n",strerror(eno));
	}
	exit(1);
}

static int is_zero(const char *p)
{
	for (;*p;++p)
		if (*p != '0')
			return 0;
	return 1;
}

static void fold(const char *text, int width)
{
	int pos, bpos = 0;
	const char *p;
	char line[1024];

	if (width > 1023) {
		printf("%s\n", text);
		return;
	}
	p = text;
	pos = 0;
	while (p[pos]) {
		while (!isspace(p[pos])) {
			line[pos] = p[pos];
			if (!p[pos])
				break;
			++pos;
			if (pos == width) {
				line[pos] = '\0';
				printf("%s\n", line);
				p += pos;
				pos = 0;
			}
		}
		while (pos < width) {
			line[pos] = p[pos];
			if (!p[pos]) {
				bpos = pos;
				break;
			}
			if (isspace(p[pos]))
				bpos = pos;
			++pos;
		}
		line[bpos] = '\0';
		printf("%s\n", line);
		p += bpos;
		pos = 0;
		while (p[pos] && isspace(p[pos]))
			++p;
	}
}

/* Handle common program options */
int tests_get_args(int argc,
		char *argv[],
		const char *title,
		const char *desc,
		const char *opts)
{
	int run_test = 0;
	int display_help = 0;
	int display_title = 0;
	int display_description = 0;
	int i;
	char *s;

	program_name = argv[0];

	s = getenv("TEST_FILE_SYSTEM_MOUNT_DIR");
	if (s)
		tests_file_system_mount_dir = strdup(s);
	s = getenv("TEST_FILE_SYSTEM_TYPE");
	if (s)
		tests_file_system_type = strdup(s);

	run_test = 1;
	rootok = 1;
	for (i = 1; i < argc; ++i) {
		if (strcmp(argv[i], "--help") == 0 ||
				strcmp(argv[i], "-h") == 0)
			display_help = 1;
		else if (strcmp(argv[i], "--title") == 0 ||
				strcmp(argv[i], "-t") == 0)
			display_title = 1;
		else if (strcmp(argv[i], "--description") == 0 ||
				strcmp(argv[i], "-d") == 0)
			display_description = 1;
		else if (strcmp(argv[i], "--sync") == 0 ||
				strcmp(argv[i], "-s") == 0)
			tests_ok_to_sync = 1;
		else if (strncmp(argv[i], "--size", 6) == 0 ||
				strncmp(argv[i], "-z", 2) == 0) {
			int64_t n;
			char *p;
			if (i+1 < argc && !isdigit(argv[i][strlen(argv[i])-1]))
				++i;
			p = argv[i];
			while (*p && !isdigit(*p))
				++p;
			n = atoll(p);
			if (n)
				tests_size_parameter = n;
			else {
				int all_zero = 1;
				for (; all_zero && *p; ++p)
					if (*p != '0')
						all_zero = 0;
				if (all_zero)
					tests_size_parameter = 0;
				else
					display_help = 1;
			}
		} else if (strncmp(argv[i], "--repeat", 8) == 0 ||
				strncmp(argv[i], "-n", 2) == 0) {
			int64_t n;
			char *p;
			if (i+1 < argc && !isdigit(argv[i][strlen(argv[i])-1]))
				++i;
			p = argv[i];
			while (*p && !isdigit(*p))
				++p;
			n = atoll(p);
			if (n || is_zero(p))
				tests_repeat_parameter = n;
			else
				display_help = 1;
		} else if (strncmp(argv[i], "--sleep", 7) == 0 ||
				strncmp(argv[i], "-p", 2) == 0) {
			int64_t n;
			char *p;
			if (i+1 < argc && !isdigit(argv[i][strlen(argv[i])-1]))
				++i;
			p = argv[i];
			while (*p && !isdigit(*p))
				++p;
			n = atoll(p);
			if (n || is_zero(p))
				tests_sleep_parameter = n;
			else
				display_help = 1;
		} else if (strcmp(argv[i], "--unlink") == 0 ||
				strcmp(argv[i], "-u") == 0)
			tests_unlink_flag = 1;
		else if (strcmp(argv[i], "--hole") == 0 ||
				strcmp(argv[i], "-o") == 0)
			tests_hole_flag = 1;
		else if (strcmp(argv[i], "--close") == 0 ||
				strcmp(argv[i], "-c") == 0)
			tests_close_flag = 1;
		else if (strcmp(argv[i], "--delete") == 0 ||
				strcmp(argv[i], "-e") == 0)
			tests_delete_flag = 1;
		else
			display_help = 1;
	}

	if (display_help) {
		run_test = 0;
		display_title = 0;
		display_description = 0;
		if (!opts)
			opts = "";
		printf("File System Test Program\n\n");
		printf("Test Title: %s\n\n", title);
		printf("Usage is: %s [ options ]\n",argv[0]);
		printf("    Options are:\n");
		printf("        -h, --help            ");
		printf("Display this help\n");
		printf("        -t, --title           ");
		printf("Display the test title\n");
		printf("        -d, --description     ");
		printf("Display the test description\n");
		if (strchr(opts, 's')) {
			printf("        -s, --sync            ");
			printf("Make use of fsync\n");
		}
		if (strchr(opts, 'z')) {
			printf("        -z, --size            ");
			printf("Set size parameter\n");
		}
		if (strchr(opts, 'n')) {
			printf("        -n, --repeat          ");
			printf("Set repeat parameter\n");
		}
		if (strchr(opts, 'p')) {
			printf("        -p, --sleep           ");
			printf("Set sleep parameter\n");
		}
		if (strchr(opts, 'u')) {
			printf("        -u, --unlink          ");
			printf("Unlink file\n");
		}
		if (strchr(opts, 'o')) {
			printf("        -o, --hole            ");
			printf("Create a hole in a file\n");
		}
		if (strchr(opts, 'c')) {
			printf("        -c, --close           ");
			printf("Close file\n");
		}
		if (strchr(opts, 'e')) {
			printf("        -e, --delete          ");
			printf("Delete file\n");
		}
		printf("\nBy default, testing is done in directory ");
		printf("/mnt/test_file_system. To change this\nuse ");
		printf("environmental variable ");
		printf("TEST_FILE_SYSTEM_MOUNT_DIR. By default, ");
		printf("the file\nsystem tested is jffs2. To change this ");
		printf("set TEST_FILE_SYSTEM_TYPE.\n\n");
		printf("Test Description:\n");
		fold(desc, 80);
	} else {
		if (display_title)
			printf("%s\n", title);
		if (display_description)
			printf("%s\n", desc);
		if (display_title || display_description)
			if (argc == 2 || (argc == 3 &&
					display_title &&
					display_description))
				run_test = 0;
	}
	return run_test;
}

/* Return the number of files (or directories) in the given directory */
unsigned tests_count_files_in_dir(const char *dir_name)
{
	DIR *dir;
	struct dirent *entry;
	unsigned count = 0;

	dir = opendir(dir_name);
	CHECK(dir != NULL);
	for (;;) {
		errno = 0;
		entry = readdir(dir);
		if (entry) {
			if (strcmp(".",entry->d_name) != 0 &&
					strcmp("..",entry->d_name) != 0)
				++count;
		} else {
			CHECK(errno == 0);
			break;
		}
	}
	CHECK(closedir(dir) != -1);
	return count;
}

/* Change to the file system mount directory, check that it is empty,
   matches the file system type, and is not the root file system */
void tests_check_test_file_system(void)
{
	struct statfs fs_info;
	struct stat f_info;
	struct stat root_f_info;

	if (chdir(tests_file_system_mount_dir) == -1 ||
			statfs(tests_file_system_mount_dir, &fs_info) == -1) {
		fprintf(stderr, "Invalid test file system mount directory:"
			" %s\n", tests_file_system_mount_dir);
		fprintf(stderr,	"Use environment variable "
			"TEST_FILE_SYSTEM_MOUNT_DIR\n");
		CHECK(0);
	}
	tests_max_fname_len = fs_info.f_namelen;
	if (strcmp(tests_file_system_type, "jffs2") == 0 &&
			fs_info.f_type != JFFS2_SUPER_MAGIC) {
		fprintf(stderr,	"File system type is not jffs2\n");
		CHECK(0);
	}
	/* Check that the test file system is not the root file system */
	if (!rootok) {
		CHECK(stat(tests_file_system_mount_dir, &f_info) != -1);
		CHECK(stat("/", &root_f_info) != -1);
		CHECK(f_info.st_dev != root_f_info.st_dev);
	}
}

/* Get the free space for the file system of the current directory */
uint64_t tests_get_free_space(void)
{
	struct statvfs fs_info;

	CHECK(statvfs(tests_file_system_mount_dir, &fs_info) != -1);
	return (uint64_t) fs_info.f_bavail * (uint64_t) fs_info.f_frsize;
}

/* Get the total space for the file system of the current directory */
uint64_t tests_get_total_space(void)
{
	struct statvfs fs_info;

	CHECK(statvfs(tests_file_system_mount_dir, &fs_info) != -1);
	return (uint64_t) fs_info.f_blocks * (uint64_t) fs_info.f_frsize;
}

#define WRITE_BUFFER_SIZE 32768

static char write_buffer[WRITE_BUFFER_SIZE];

static void init_write_buffer()
{
	static int init = 0;

	if (!init) {
		int i, d;
		uint64_t u;

		u = RAND_MAX;
		u += 1;
		u /= 256;
		d = (int) u;
		srand(1);
		for (i = 0; i < WRITE_BUFFER_SIZE; ++i)
			write_buffer[i] = rand() / d;
		init = 1;
	}
}

/* Write size random bytes into file descriptor fd at the current position,
   returning the number of bytes actually written */
uint64_t tests_fill_file(int fd, uint64_t size)
{
	ssize_t written;
	size_t sz;
	unsigned start = 0, length;
	uint64_t remains;
	uint64_t actual_size = 0;

	init_write_buffer();
	remains = size;
	while (remains > 0) {
		length = WRITE_BUFFER_SIZE - start;
		if (remains > length)
			sz = length;
		else
			sz = (size_t) remains;
		written = write(fd, write_buffer + start, sz);
		if (written <= 0) {
			CHECK(errno == ENOSPC); /* File system full */
			errno = 0;
			break;
		}
		remains -= written;
		actual_size += written;
		if (written == sz)
			start = 0;
		else
			start += written;
	}
	tests_maybe_sync(fd);
	return actual_size;
}

/* Write size random bytes into file descriptor fd at offset,
   returning the number of bytes actually written */
uint64_t tests_write_filled_file(int fd, off_t offset, uint64_t size)
{
	ssize_t written;
	size_t sz;
	unsigned start = 0, length;
	uint64_t remains;
	uint64_t actual_size = 0;

	CHECK(lseek(fd, offset, SEEK_SET) == offset);

	init_write_buffer();
	remains = size;
	start = offset % WRITE_BUFFER_SIZE;
	while (remains > 0) {
		length = WRITE_BUFFER_SIZE - start;
		if (remains > length)
			sz = length;
		else
			sz = (size_t) remains;
		written = write(fd, write_buffer + start, sz);
		if (written <= 0) {
			CHECK(errno == ENOSPC); /* File system full */
			errno = 0;
			break;
		}
		remains -= written;
		actual_size += written;
		if (written == sz)
			start = 0;
		else
			start += written;
	}
	tests_maybe_sync(fd);
	return actual_size;
}

/* Check that a file written using tests_fill_file() and/or
   tests_write_filled_file() and/or tests_create_file()
   contains the expected random data */
void tests_check_filled_file_fd(int fd)
{
	ssize_t sz;
	char buf[WRITE_BUFFER_SIZE];

	CHECK(lseek(fd, 0, SEEK_SET) == 0);
	do {
		sz = read(fd, buf, WRITE_BUFFER_SIZE);
		CHECK(sz >= 0);
		CHECK(memcmp(buf, write_buffer, sz) == 0);
	} while (sz);
}

/* Check that a file written using tests_fill_file() and/or
   tests_write_filled_file() and/or tests_create_file()
   contains the expected random data */
void tests_check_filled_file(const char *file_name)
{
	int fd;

	fd = open(file_name, O_RDONLY);
	CHECK(fd != -1);
	tests_check_filled_file_fd(fd);
	CHECK(close(fd) != -1);
}

static void tests_sync_directory(const char *file_name)
{
	char *path;
	char *dir;
	int fd;

	if (!tests_ok_to_sync)
		return;

	path = strdup(file_name);
	dir = dirname(path);
	fd = open(dir,O_RDONLY | tests_maybe_sync_flag());
	CHECK(fd != -1);
	CHECK(fsync(fd) != -1);
	CHECK(close(fd) != -1);
	free(path);
}

/* Delete a file */
void tests_delete_file(const char *file_name)
{
	CHECK(unlink(file_name) != -1);
	tests_sync_directory(file_name);
}

/* Create a file of size file_size */
uint64_t tests_create_file(const char *file_name, uint64_t file_size)
{
	int fd;
	int flags;
	mode_t mode;
	uint64_t actual_size; /* Less than size if the file system is full */

	flags = O_CREAT | O_TRUNC | O_WRONLY | tests_maybe_sync_flag();
	mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH;
	fd = open(file_name, flags, mode);
	if (fd == -1 && errno == ENOSPC) {
		errno = 0;
		return 0; /* File system full */
	}
	CHECK(fd != -1);
	actual_size = tests_fill_file(fd, file_size);
	CHECK(close(fd) != -1);
	if (file_size != 0 && actual_size == 0)
		tests_delete_file(file_name);
	else
		tests_sync_directory(file_name);
	return actual_size;
}

/* Calculate: free_space * numerator / denominator */
uint64_t tests_get_big_file_size(unsigned numerator, unsigned denominator)
{
	if (denominator == 0)
		denominator = 1;
	if (numerator > denominator)
		numerator = denominator;
	return numerator * (tests_get_free_space() / denominator);
}

/* Create file "fragment_n" where n is the file_number, and unlink it */
int tests_create_orphan(unsigned file_number)
{
	int fd;
	int flags;
	mode_t mode;
	char file_name[256];

	sprintf(file_name, "fragment_%u", file_number);
	flags = O_CREAT | O_TRUNC | O_RDWR | tests_maybe_sync_flag();
	mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH;
	fd = open(file_name, flags, mode);
	if (fd == -1 && (errno == ENOSPC || errno == EMFILE))
		return fd; /* File system full or too many open files */
	CHECK(fd != -1);
	tests_sync_directory(file_name);
	CHECK(unlink(file_name) != -1);
	return fd;
}

/* Write size bytes at offset to the file "fragment_n" where n is the
   file_number and file_number also determines the random data written
   i.e. seed for random numbers */
unsigned tests_write_fragment_file(unsigned file_number,
				int fd,
				off_t offset,
				unsigned size)
{
	int i, d;
	uint64_t u;
	ssize_t written;
	off_t pos;
	char buf[WRITE_BUFFER_SIZE];

	if (size > WRITE_BUFFER_SIZE)
		size = WRITE_BUFFER_SIZE;

	pos = lseek(fd, 0, SEEK_END);
	CHECK(pos != (off_t) -1);
	if (offset > pos)
		offset = pos;

	pos = lseek(fd, offset, SEEK_SET);
	CHECK(pos != (off_t) -1);
	CHECK(pos == offset);

	srand(file_number);
	while (offset--)
		rand();

	u = RAND_MAX;
	u += 1;
	u /= 256;
	d = (int) u;
	for (i = 0; i < size; ++i)
		buf[i] = rand() / d;

	written = write(fd, buf, size);
	if (written <= 0) {
		CHECK(errno == ENOSPC); /* File system full */
		errno = 0;
		written = 0;
	}
	tests_maybe_sync(fd);
	return (unsigned) written;
}

/* Write size bytes to the end of file descriptor fd using file_number
   to determine the random data written i.e. seed for random numbers */
unsigned tests_fill_fragment_file(unsigned file_number, int fd, unsigned size)
{
	off_t offset;

	offset = lseek(fd, 0, SEEK_END);
	CHECK(offset != (off_t) -1);

	return tests_write_fragment_file(file_number, fd, offset, size);
}

/* Write size bytes to the end of file "fragment_n" where n is the file_number
   and file_number also determines the random data written
   i.e. seed for random numbers */
unsigned tests_append_to_fragment_file(unsigned file_number,
					unsigned size,
					int create)
{
	int fd;
	int flags;
	mode_t mode;
	unsigned actual_growth;
	char file_name[256];

	sprintf(file_name, "fragment_%u", file_number);
	if (create)
		flags = O_CREAT | O_EXCL | O_WRONLY | tests_maybe_sync_flag();
	else
		flags = O_WRONLY | tests_maybe_sync_flag();
	mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH;
	fd = open(file_name, flags, mode);
	if (fd == -1 && errno == ENOSPC) {
		errno = 0;
		return 0; /* File system full */
	}
	CHECK(fd != -1);
	actual_growth = tests_fill_fragment_file(file_number, fd, size);
	CHECK(close(fd) != -1);
	if (create && !actual_growth)
		tests_delete_fragment_file(file_number);
	return actual_growth;
}

/* Write size bytes at offset to the file "fragment_n" where n is the
   file_number and file_number also determines the random data written
   i.e. seed for random numbers */
unsigned tests_overwite_fragment_file(	unsigned file_number,
					off_t offset,
					unsigned size)
{
	int fd;
	unsigned actual_size;
	char file_name[256];

	sprintf(file_name, "fragment_%u", file_number);
	fd = open(file_name, O_RDWR | tests_maybe_sync_flag());
	if (fd == -1 && errno == ENOSPC) {
		errno = 0;
		return 0; /* File system full */
	}
	CHECK(fd != -1);
	actual_size = tests_write_fragment_file(file_number,
		fd, offset, size);
	CHECK(close(fd) != -1);
	return actual_size;
}

/* Delete file "fragment_n" where n is the file_number */
void tests_delete_fragment_file(unsigned file_number)
{
	char file_name[256];

	sprintf(file_name, "fragment_%u", file_number);
	tests_delete_file(file_name);
}

/* Check the random data in file "fragment_n" is what is expected */
void tests_check_fragment_file_fd(unsigned file_number, int fd)
{
	ssize_t sz, i;
	int d;
	uint64_t u;
	char buf[8192];

	CHECK(lseek(fd, 0, SEEK_SET) == 0);
	srand(file_number);
	u = RAND_MAX;
	u += 1;
	u /= 256;
	d = (int) u;
	for (;;) {
		sz = read(fd, buf, 8192);
		if (sz == 0)
			break;
		CHECK(sz >= 0);
		for (i = 0; i < sz; ++i)
			CHECK(buf[i] == (char) (rand() / d));
	}
}

/* Check the random data in file "fragment_n" is what is expected */
void tests_check_fragment_file(unsigned file_number)
{
	int fd;
	ssize_t sz, i;
	int d;
	uint64_t u;
	char file_name[256];
	char buf[8192];

	sprintf(file_name, "fragment_%u", file_number);
	fd = open(file_name, O_RDONLY);
	CHECK(fd != -1);
	srand(file_number);
	u = RAND_MAX;
	u += 1;
	u /= 256;
	d = (int) u;
	for (;;) {
		sz = read(fd, buf, 8192);
		if (sz == 0)
			break;
		CHECK(sz >= 0);
		for (i = 0; i < sz; ++i)
			CHECK(buf[i] == (char) (rand() / d));
	}
	CHECK(close(fd) != -1);
}

/* Central point to decide whether to use fsync */
void tests_maybe_sync(int fd)
{
	if (tests_ok_to_sync)
		CHECK(fsync(fd) != -1);
}

/* Return O_SYNC if ok to sync otherwise return 0 */
int tests_maybe_sync_flag(void)
{
	if (tests_ok_to_sync)
		return O_SYNC;
	return 0;
}

/* Return random number from 0 to n - 1 */
size_t tests_random_no(size_t n)
{
	uint64_t a, b;

	if (!n)
		return 0;
	if (n - 1 <= RAND_MAX) {
		a = rand();
		b = RAND_MAX;
		b += 1;
	} else {
		const uint64_t u = 1 + (uint64_t) RAND_MAX;
		a = rand();
		a *= u;
		a += rand();
		b = u * u;
		CHECK(n <= b);
	}
	if (RAND_MAX <= UINT32_MAX && n <= UINT32_MAX)
		return a * n / b;
	else /*if (RAND_MAX <= UINT64_MAX && n <= UINT64_MAX)*/ {
		uint64_t x, y;
		if (a < n) {
			x = a;
			y = n;
		} else {
			x = n;
			y = a;
		}
		return (x * (y / b)) + ((x * (y % b)) / b);
	}
}

/* Make a directory empty */
void tests_clear_dir(const char *dir_name)
{
	DIR *dir;
	struct dirent *entry;
	char buf[4096];

	dir = opendir(dir_name);
	CHECK(dir != NULL);
	CHECK(getcwd(buf, 4096) != NULL);
	CHECK(chdir(dir_name) != -1);
	for (;;) {
		errno = 0;
		entry = readdir(dir);
		if (entry) {
			if (strcmp(".",entry->d_name) != 0 &&
					strcmp("..",entry->d_name) != 0) {
				if (entry->d_type == DT_DIR) {
					tests_clear_dir(entry->d_name);
					CHECK(rmdir(entry->d_name) != -1);
				} else
					CHECK(unlink(entry->d_name) != -1);
			}
		} else {
			CHECK(errno == 0);
			break;
		}
	}
	CHECK(chdir(buf) != -1);
	CHECK(closedir(dir) != -1);
}

/* Create an empty sub-directory or small file in the current directory */
int64_t tests_create_entry(char *return_name)
{
	int fd;
	char name[256];

	for (;;) {
		sprintf(name, "%u", (unsigned) tests_random_no(10000000));
		fd = open(name, O_RDONLY);
		if (fd == -1)
			break;
		close(fd);
	}
	if (return_name)
		strcpy(return_name, name);
	if (tests_random_no(2)) {
		return tests_create_file(name, tests_random_no(4096));
	} else {
		if (mkdir(name, 0777) == -1) {
			CHECK(errno == ENOSPC);
			errno = 0;
			return 0;
		}
		return TESTS_EMPTY_DIR_SIZE;
	}
}

/* Remove a random file of empty sub-directory from the current directory */
int64_t tests_remove_entry(void)
{
	DIR *dir;
	struct dirent *entry;
	unsigned count = 0, pos;
	int64_t result = 0;

	dir = opendir(".");
	CHECK(dir != NULL);
	for (;;) {
		errno = 0;
		entry = readdir(dir);
		if (entry) {
			if (strcmp(".",entry->d_name) != 0 &&
					strcmp("..",entry->d_name) != 0)
				++count;
		} else {
			CHECK(errno == 0);
			break;
		}
	}
	pos = tests_random_no(count);
	count = 0;
	rewinddir(dir);
	for (;;) {
		errno = 0;
		entry = readdir(dir);
		if (!entry) {
			CHECK(errno == 0);
			break;
		}
		if (strcmp(".",entry->d_name) != 0 &&
				strcmp("..",entry->d_name) != 0) {
			if (count == pos) {
				if (entry->d_type == DT_DIR) {
					tests_clear_dir(entry->d_name);
					CHECK(rmdir(entry->d_name) != -1);
					result = TESTS_EMPTY_DIR_SIZE;
				} else {
					struct stat st;
					CHECK(stat(entry->d_name, &st) != -1);
					result = st.st_size;
					CHECK(unlink(entry->d_name) != -1);
				}
			}
			++count;
		}
	}
	CHECK(closedir(dir) != -1);
	return result;
}

/* Read mount information from /proc/mounts or /etc/mtab */
static int tests_get_mount_info(struct mntent *info)
{
	FILE *f;
	struct mntent *entry;
	int found = 0;

	f = fopen("/proc/mounts", "rb");
	if (!f)
		f = fopen("/etc/mtab", "rb");
	CHECK(f != NULL);
	while (!found) {
		entry = getmntent(f);
		if (entry) {
			if (strcmp(entry->mnt_dir,
				tests_file_system_mount_dir) == 0) {
				found = 1;
				*info = *entry;
			}
		} else
			break;
	}
	CHECK(fclose(f) == 0);
	return found;
}

/*
 * This funcion parses file-system options string, extracts standard mount
 * options from there, and saves them in the @flags variable. The non-standard
 * (fs-specific) mount options are left in @mnt_opts string, while the standard
 * ones will be removed from it.
 *
 * The reason for this perverted function is that we want to preserve mount
 * options when unmounting the file-system and mounting it again. But we cannot
 * pass standard* mount optins (like sync, ro, etc) as a string to the
 * 'mount()' function, because it fails. It accepts standard mount options only
 * as flags. And only the FS-specific mount options are accepted in form of a
 * string.
 */
static int process_mount_options(char **mnt_opts, unsigned long *flags)
{
	char *tmp, *opts, *p;
	const char *opt;

	/*
	 * We are going to use 'strtok()' which modifies the original string,
	 * so duplicate it.
	 */
	tmp = strdup(*mnt_opts);
	if (!tmp)
		goto out_mem;

	p = opts = calloc(1, strlen(*mnt_opts) + 1);
	if (!opts) {
		free(tmp);
		goto out_mem;
	}

	*flags = 0;
	opt = strtok(tmp, ",");
	while (opt) {
		if (!strcmp(opt, "rw"))
			;
		else if (!strcmp(opt, "ro"))
			*flags |= MS_RDONLY;
		else if (!strcmp(opt, "dirsync"))
			*flags |= MS_DIRSYNC;
		else if (!strcmp(opt, "noatime"))
			*flags |= MS_NOATIME;
		else if (!strcmp(opt, "nodiratime"))
			*flags |= MS_NODIRATIME;
		else if (!strcmp(opt, "noexec"))
			*flags |= MS_NOEXEC;
		else if (!strcmp(opt, "nosuid"))
			*flags |= MS_NOSUID;
		else if (!strcmp(opt, "relatime"))
			*flags |= MS_RELATIME;
		else if (!strcmp(opt, "sync"))
			*flags |= MS_SYNCHRONOUS;
		else {
			int len = strlen(opt);

			if (p != opts)
				*p++ = ',';
			memcpy(p, opt, len);
			p += len;
			*p = '\0';
		}

		opt = strtok(NULL, ",");
	}

	free(tmp);
	*mnt_opts = opts;
	return 0;

out_mem:
	fprintf(stderr, "cannot allocate memory\n");
	return 1;
}

/*
 * Re-mount test file system. Randomly choose how to do this: re-mount R/O then
 * re-mount R/W, or unmount, then mount R/W, or unmount then mount R/O then
 * re-mount R/W, etc. This should improve test coverage.
 */
void tests_remount(void)
{
	int err;
	struct mntent mount_info;
	const char *source, *target, *filesystemtype;
	char cwd[4096], *data;
	unsigned long mountflags, flags;
	unsigned int rorw1, um, um_ro, um_rorw, rorw2;

	CHECK(tests_get_mount_info(&mount_info));

	if (strcmp(mount_info.mnt_dir,"/") == 0)
		return;

	/* Save current working directory */
	CHECK(getcwd(cwd, 4096) != NULL);
	/* Temporarily change working directory to '/' */
	CHECK(chdir("/") != -1);

	/* Choose what to do */
	rorw1 = tests_random_no(2);
	um = tests_random_no(2);
	um_ro = tests_random_no(2);
	um_rorw = tests_random_no(2);
	rorw2 = tests_random_no(2);

	if (rorw1 + um + rorw2 == 0)
		um = 1;

	source = mount_info.mnt_fsname;
	target = tests_file_system_mount_dir;
	filesystemtype = tests_file_system_type;
	data = mount_info.mnt_opts;
	process_mount_options(&data, &mountflags);

	if (rorw1) {
		/* Re-mount R/O and then re-mount R/W */
		flags = mountflags | MS_RDONLY | MS_REMOUNT;
		err = mount(source, target, filesystemtype, flags, data);
		CHECK(err != -1);

		flags = mountflags | MS_REMOUNT;
		flags &= ~((unsigned long)MS_RDONLY);
		err = mount(source, target, filesystemtype, flags, data);
		CHECK(err != -1);
	}

	if (um) {
		/* Unmount and mount */
		if (um_ro) {
			/* But re-mount R/O before unmounting */
			flags = mountflags | MS_RDONLY | MS_REMOUNT;
			err = mount(source, target, filesystemtype,
				    flags, data);
			CHECK(err != -1);
		}

		CHECK(umount(target) != -1);

		if (!um_rorw) {
			/* Mount R/W straight away */
			err = mount(source, target, filesystemtype,
				    mountflags, data);
			CHECK(err != -1);
		} else {
			/* Mount R/O and then re-mount R/W */
			err = mount(source, target, filesystemtype,
				    mountflags | MS_RDONLY, data);
			CHECK(err != -1);

			flags = mountflags | MS_REMOUNT;
			flags &= ~((unsigned long)MS_RDONLY);
			err = mount(source, target, filesystemtype,
				    flags, data);
			CHECK(err != -1);
		}
	}

	if (rorw2) {
		/* Re-mount R/O and then re-mount R/W */
		flags = mountflags | MS_RDONLY | MS_REMOUNT;
		err = mount(source, target, filesystemtype, flags, data);
		CHECK(err != -1);

		flags = mountflags | MS_REMOUNT;
		flags &= ~((unsigned long)MS_RDONLY);
		err = mount(source, target, filesystemtype, flags, data);
		CHECK(err != -1);
	}

	/* Restore the previous working directory */
	CHECK(chdir(cwd) != -1);
}

/* Un-mount or re-mount test file system */
static void tests_mnt(int mnt)
{
	static struct mntent mount_info;
	const char *source;
	const char *target;
	const char *filesystemtype;
	unsigned long mountflags;
	char *data;
	static char cwd[4096];

	if (mnt == 0) {
		CHECK(tests_get_mount_info(&mount_info));
		if (strcmp(mount_info.mnt_dir,"/") == 0)
			return;
		CHECK(getcwd(cwd, 4096) != NULL);
		CHECK(chdir("/") != -1);
		CHECK(umount(tests_file_system_mount_dir) != -1);
	} else {
		source = mount_info.mnt_fsname;
		target = tests_file_system_mount_dir;
		filesystemtype = tests_file_system_type;
		data = mount_info.mnt_opts;
		process_mount_options(&data, &mountflags);

		CHECK(mount(source, target, filesystemtype, mountflags, data)
			!= -1);
		CHECK(chdir(cwd) != -1);
	}
}

/* Unmount test file system */
void tests_unmount(void)
{
	tests_mnt(0);
}

/* Mount test file system */
void tests_mount(void)
{
	tests_mnt(1);
}

/* Check whether the test file system is also the root file system */
int tests_fs_is_rootfs(void)
{
	struct stat f_info;
	struct stat root_f_info;

	CHECK(stat(tests_file_system_mount_dir, &f_info) != -1);
	CHECK(stat("/", &root_f_info) != -1);
	if (f_info.st_dev == root_f_info.st_dev)
		return 1;
	else
		return 0;
}

/* Try to make a directory empty */
void tests_try_to_clear_dir(const char *dir_name)
{
	DIR *dir;
	struct dirent *entry;
	char buf[4096];

	dir = opendir(dir_name);
	if (dir == NULL)
		return;
	if (getcwd(buf, 4096) == NULL || chdir(dir_name) == -1) {
		closedir(dir);
		return;
	}
	for (;;) {
		errno = 0;
		entry = readdir(dir);
		if (entry) {
			if (strcmp(".",entry->d_name) != 0 &&
					strcmp("..",entry->d_name) != 0) {
				if (entry->d_type == DT_DIR) {
					tests_try_to_clear_dir(entry->d_name);
					rmdir(entry->d_name);
				} else
					unlink(entry->d_name);
			}
		} else {
			CHECK(errno == 0);
			break;
		}
	}
	if (chdir(buf) < 0)
		perror("chdir");
	closedir(dir);
}

/* Check whether the test file system is also the current file system */
int tests_fs_is_currfs(void)
{
	struct stat f_info;
	struct stat curr_f_info;

	CHECK(stat(tests_file_system_mount_dir, &f_info) != -1);
	CHECK(stat(".", &curr_f_info) != -1);
	if (f_info.st_dev == curr_f_info.st_dev)
		return 1;
	else
		return 0;
}

#define PID_BUF_SIZE 64

/* Concatenate a pid to a string in a signal safe way */
void tests_cat_pid(char *buf, const char *name, pid_t pid)
{
	char *p;
	unsigned x;
	const char digits[] = "0123456789";
	char pid_buf[PID_BUF_SIZE];

	x = (unsigned) pid;
	p = pid_buf + PID_BUF_SIZE;
	*--p = '\0';
	if (x)
		while (x) {
			*--p = digits[x % 10];
			x /= 10;
		}
	else
		*--p = '0';
	buf[0] = '\0';
	strcat(buf, name);
	strcat(buf, p);
}
