/*
 *
 *   Copyright (c) International Business Machines  Corp., 2001
 *
 *   This program is free software;  you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   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 Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

/******************************************************************************/
/*                                                                            */
/* File:         mmstress.c                                                   */
/*                                                                            */
/* Description:  This is a test program that performs general stress with     */
/*               memory race conditions. It contains seven testcases that     */
/*               will test race conditions between simultaneous read fault,   */
/*               write fault, copy on write (COW) fault e.t.c.                */
/*               This testcase is intended to execute on the Linux operating  */
/*               system and can be easily ported to work on other operating   */
/*               systems as well.                                             */
/*                                                                            */
/* Usage:        mmstress -h -n TEST NUMBER -p NPAGES -t EXECUTION TIME -v -V */
/*                        -h                - Help                            */
/*                        -n TEST NUMBER    - Execute a particular testcase   */
/*                        -p NPAGES         - Use NPAGES pages for tests    */
/*                        -t EXECUTION TIME - Execute test for a certain time */
/*                        -v                - Verbose output                  */
/*                        -V                - Version of this program         */
/*                                                                            */
/* Author:       Manoj Iyer - manjo@mail.utexas.edu                           */
/*                                                                            */
/******************************************************************************/

/******************************************************************************/
/*                                                                            */
/* Apr-13-2001    Created: Manoj Iyer, IBM Austin.                            */
/*        These tests are adapted from AIX vmm FVT tests.                     */
/*                                                                            */
/* Oct-24-2001  Modified.                                                     */
/*        - freed buffers that were allocated.                                */
/*        - closed removed files. This will remove the disk full error        */
/*        - use pthread_exit in case of theads instead of return. This        */
/*          was really bad to use return!                                     */
/*        - created usage function.                                           */
/*        - pthread_join checks for thread exit status reported by            */
/*          pthread_exit()                                                    */
/*                                                                            */
/* Oct-25-2001  Modified.                                                     */
/*        - Fixed bug in usage()                                              */
/*        - malloc'ed pointer for pthread return value.                       */
/*        - changed scheme. If no options are specified, all the tests        */
/*          will be run once.                                                 */
/*                                                                            */
/* Nov-02-2001  Modified - Paul Larson                                        */
/*        - Added sched_yield to thread_fault to fix hang                     */
/*        - Removed thread_mmap                                               */
/*                                                                            */
/* Nov-09-2001  Modified - Manoj Iyer                                         */
/*        - Removed compile warnings.                                         */
/*        - Added missing header file. #include <stdlib.h>                    */
/*                                                                            */
/* Oct-28-2003  Modified - Manoj Iyer                                         */
/*        - missing parenthesis added.                                        */
/*        - formatting changes.                                               */
/*        - increased NUMPAGES to 9999.                                       */
/*                                                                            */
/* Jan-30-2003  Modified - Gary Williams                                      */
/*        - fixed a race condition between the two threads                    */
/*        - made it so if any of the testcases fail the test will fail        */
/*        - fixed so status of child in test 6 is used to determine result    */
/*        - fixed the use of the remove_files function in a conditional       */
/*                                                                            */
/******************************************************************************/

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <pthread.h>
#include <signal.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sched.h>
#include <stdint.h>
#include <getopt.h>

#include "test.h"

/* GLOBAL DEFINES                                                             */
#define SIGENDSIG    -1		/* end of signal marker                             */
#define THNUM        0		/* array element pointing to number of threads      */
#define MAPADDR      1		/* array element pointing to map address            */
#define PAGESIZ      2		/* array element pointing to page size              */
#define FLTIPE       3		/* array element pointing to fault type             */
#define READ_FAULT   0		/* instructs routine to simulate read fault         */
#define WRITE_FAULT  1		/* instructs routine to simulate write fault        */
#define COW_FAULT    2		/* instructs routine to simulate copy-on-write fault */
#define NUMTHREAD    32		/* number of threads to spawn default to 32         */
#define NUMPAGES     9999	/* default (random) value of number of pages        */
#ifndef TRUE
#define TRUE         1
#endif
#ifndef FALSE
#define FALSE        0
#endif
#define FAILED       (-1)	/* return status for all funcs indicating failure   */
#define SUCCESS      0		/* return status for all routines indicating success */

#define BRKSZ        512*1024	/* program data space allocation value          */

static volatile int wait_thread;	/* used to wake up sleeping threads    */
static volatile int thread_begin;	/* used to coordinate threads          */
static int verbose_print = FALSE;	/* print more test information           */

static int pages_num = NUMPAGES;	/* number of pages to use for tests     */
static volatile int alarm_fired;

char *TCID = "mmstress";
int TST_TOTAL = 6;

static void sig_handler(int signal)
{
	if (signal != SIGALRM) {
		fprintf(stderr,
			"sig_handlder(): unexpected signal caught [%d]\n",
			signal);
		exit(TBROK);
	}

	alarm_fired = 1;
}

static void usage(char *progname)
{
	fprintf(stderr, "usage:%s -h -n test -t time -v [-V]\n", progname);
	fprintf(stderr, "\t-h displays all options\n");
	fprintf(stderr, "\t-n test number, if no test number\n"
		"\t   is specified, all the tests will be run\n");
	fprintf(stderr, "\t-p specify the number of pages to\n"
		"\t   use for allocation\n");
	fprintf(stderr, "\t-t specify the time in hours\n");
	fprintf(stderr, "\t-v verbose output\n");
	fprintf(stderr, "\t-V program version\n");
	exit(1);
}

static void set_timer(int run_time)
{
	struct itimerval timer;

	memset(&timer, 0, sizeof(struct itimerval));
	timer.it_interval.tv_usec = 0;
	timer.it_interval.tv_sec = 0;
	timer.it_value.tv_usec = 0;
	timer.it_value.tv_sec = (time_t) (run_time * 3600.0);

	if (setitimer(ITIMER_REAL, &timer, NULL)) {
		perror("set_timer(): setitimer()");
		exit(1);
	}
}

/******************************************************************************/
/*                                                                            */
/* Function:    thread_fault                                                  */
/*                                                                            */
/* Description: Executes as a thread function and accesses the memory pages   */
/*              depending on the fault_type to be generated. This function    */
/*              can cause READ fault, WRITE fault, COW fault.                 */
/*                                                                            */
/* Input:       void *args - argments passed to the exec routine by           */
/*              pthread_create()                                              */
/*                                                                            */
/******************************************************************************/
static void *thread_fault(void *args)
{
	long *local_args = args;	/* local pointer to list of arguments        */
	/* local_args[THNUM]   - the thread number   */
	/* local_args[MAPADDR] - map address         */
	/* local_args[PAGESIZ] - page size           */
	/* local_args[FLTIPE]  - fault type          */
	int pgnum_ndx = 0;	/* index to the number of pages              */
	char *start_addr	/* start address of the page                 */
	    = (void *) (local_args[MAPADDR]
			 + (int)local_args[THNUM]
			 * (pages_num / NUMTHREAD)
			 * local_args[PAGESIZ]);
	char read_from_addr = 0;	/* address to which read from page is done   */
	char write_to_addr[] = { 'a' };	/* character to be writen to the page    */

    /*************************************************************/
	/*   The way it was, args could be overwritten by subsequent uses
	 *   of it before this routine had a chance to use the data.
	 *   This flag stops the overwrite until this routine gets to
	 *   here.  At this point, it is done initializing and it is
	 *   safe for the parent thread to continue (which will change
	 *   args).
	 */
	thread_begin = FALSE;

	while (wait_thread)
		sched_yield();

	for (; pgnum_ndx < (pages_num / NUMTHREAD); pgnum_ndx++) {
		/* if the fault to be generated is READ_FAULT, read from the page     */
		/* else write a character to the page.                                */
		((int)local_args[3] == READ_FAULT) ? (read_from_addr =
						      *start_addr)
		    : (*start_addr = write_to_addr[0]);
		start_addr += local_args[PAGESIZ];
		if (verbose_print)
			tst_resm(TINFO,
				 "thread_fault(): generating fault type %ld"
				 " @page address %p", local_args[3],
				 start_addr);
		fflush(NULL);
	}
	pthread_exit(NULL);
}

/******************************************************************************/
/*                                                                            */
/* Function:    remove_tmpfiles                                               */
/*                                                                            */
/* Description: remove temporary files that were created by the tests.        */
/*                                                                            */
/******************************************************************************/
static int remove_files(char *filename, char *addr)
{
	if (addr)
		if (munmap(addr, sysconf(_SC_PAGESIZE) * pages_num) < 0) {
			perror("map_and_thread(): munmap()");
			return FAILED;
		}
	if (strcmp(filename, "NULL") && strcmp(filename, "/dev/zero")) {
		if (unlink(filename)) {
			perror("map_and_thread(): ulink()");
			return FAILED;
		}
	} else {
		if (verbose_print)
			tst_resm(TINFO, "file %s removed", filename);

	}
	return SUCCESS;
}

/******************************************************************************/
/*                                                                            */
/* Function:    map_and_thread                                                */
/*                                                                            */
/* Description: Creates mappings with the required properties, of MAP_PRIVATE */
/*              MAP_SHARED and of PROT_RED / PROT_READ|PROT_WRITE.            */
/*              Create threads and execute a routine that will generate the   */
/*              desired fault condition, viz, read, write and cow fault.      */
/*                                                                            */
/* Input:       char *tmpfile - name of temporary file that is created        */
/*              int   fault_type - type of fault that is to be generated.     */
/*                                                                            */
/******************************************************************************/
int map_and_thread(char *tmpfile,
			  void *(*exec_func) (void *),
			  int fault_type,
			  int num_thread)
{
	int fd = 0;		/* file descriptor of the file created       */
	int thrd_ndx = 0;	/* index to the number of threads created    */
	int map_type = 0;	/* specifies the type of the mapped object   */
	void *th_status;	/* status of the thread when it is finished  */
	long th_args[5];	/* argument list passed to  thread_fault()   */
	char *empty_buf = NULL;	/* empty buffer used to fill temp file       */
	long pagesize		/* contains page size at runtime             */
	    = sysconf(_SC_PAGESIZE);
	static pthread_t pthread_ids[NUMTHREAD];
	/* contains ids of the threads created       */
	void * map_addr = NULL;	/* address where the file is mapped          */
	ssize_t written = 0;
	ssize_t bytes;

	/* Create a file with permissions 0666, and open it with RDRW perms       */
	/* if the name is not a NULL                                              */

	if (strcmp(tmpfile, "NULL")) {
		if ((fd =
		     open(tmpfile, O_RDWR | O_CREAT,
			  S_IRWXO | S_IRWXU | S_IRWXG))
		    == -1) {
			perror("map_and_thread(): open()");
			fflush(NULL);
			return FAILED;
		}

		/* Write pagesize * pages_num bytes to the file */
		empty_buf = malloc(pagesize * pages_num);
		if (!empty_buf) {
			perror("map_and_thread(): malloc()");
			remove_files(tmpfile, NULL);
			close(fd);
			fflush(NULL);
			return FAILED;
		}

		/* Writing fewer bytes than required is not an error so retry if
		 * fewer were written; if that happened due to some permanent
		 * error like ENOSPC the following retry will fail and a proper
		 * errno will be reported.
		 */
		do {
			bytes = write(fd, empty_buf + written,
				      pagesize * pages_num - written);
			if (bytes < 0) {
				perror("map_and_thread(): write()");
				free(empty_buf);
				fflush(NULL);
				close(fd);
				remove_files(tmpfile, NULL);
				return FAILED;
			}
			written += bytes;
		} while (written < pagesize * pages_num);
		map_type = (fault_type == COW_FAULT) ? MAP_PRIVATE : MAP_SHARED;

		/* Map the file, if the required fault type is COW_FAULT map the file */
		/* private, else map the file shared. if READ_FAULT is required to be */
		/* generated map the file with read protection else map with read -   */
		/* write protection.                               */

		if ((map_addr = (void *) mmap(0, pagesize * pages_num,
					       ((fault_type == READ_FAULT) ?
						PROT_READ : PROT_READ |
						PROT_WRITE), map_type, fd, 0))
		    == MAP_FAILED) {
			perror("map_and_thread(): mmap()");
			free(empty_buf);
			fflush(NULL);
			remove_files(tmpfile, NULL);
			close(fd);
			return FAILED;
		} else {
			if (verbose_print)
				tst_resm(TINFO,
					 "map_and_thread(): mmap success, address = %p",
					 map_addr);
			fflush(NULL);
		}
	}

	/* As long as wait is set to TRUE, the thread that will be created will */
	/* loop in its exec routine */

	wait_thread = TRUE;

	/* Create a few threads, ideally number of threads equals number of CPU'S */
	/* so that we can assume that each thread will run on a single CPU in     */
	/* of SMP machines. Currently we will create NR_CPUS number of threads.   */

	th_args[1] = (long)map_addr;
	th_args[2] = pagesize;
	th_args[3] = fault_type;
	do {
		th_args[0] = thrd_ndx;
		th_args[4] = (long)0;

       /*************************************************************/
		/*   The way it was, args could be overwritten by subsequent uses
		 *   of it before the called routine had a chance to fully initialize.
		 *   This flag stops the overwrite until that routine gets to
		 *   begin.  At that point, it is done initializing and it is
		 *   safe for the this thread to continue (which will change
		 *   args).
		 *   A basic race condition.
		 */
		thread_begin = TRUE;
		if (pthread_create(&pthread_ids[thrd_ndx++], NULL, exec_func,
				   (void *)&th_args)) {
			perror("map_and_thread(): pthread_create()");
			thread_begin = FALSE;
			free(empty_buf);
			fflush(NULL);
			remove_files(tmpfile, map_addr);
			close(fd);
			return FAILED;
		} else {
	    /***************************************************/
			/*   Yield until new thread is done with args.
			 */
			while (thread_begin)
				sched_yield();
		}
	} while (thrd_ndx < num_thread);

	if (verbose_print)
		tst_resm(TINFO, "map_and_thread(): pthread_create() success");
	wait_thread = FALSE;

	/* suspend the execution of the calling thread till the execution of the  */
	/* other thread has been terminated.                                      */

	for (thrd_ndx = 0; thrd_ndx < NUMTHREAD; thrd_ndx++) {
		if (pthread_join(pthread_ids[thrd_ndx], &th_status)) {
			perror("map_and_thread(): pthread_join()");
			free(empty_buf);
			fflush(NULL);
			remove_files(tmpfile, map_addr);
			close(fd);
			return FAILED;
		} else {
			if ((long)th_status == 1) {
				tst_resm(TINFO,
					 "thread [%ld] - process exited with errors",
					 (long)pthread_ids[thrd_ndx]);
				free(empty_buf);
				remove_files(tmpfile, map_addr);
				close(fd);
				exit(1);
			}
		}
	}

	/* remove the temporary file that was created. - clean up                 */
	/* but dont try to remove special files.                                  */

    /***********************************************/
	/*   Was if !(remove_files()) ...
	 *   If that routine succeeds, it returns SUCCESS, which
	 *   happens to be 0.  So if the routine succeeded, the
	 *   above condition would indicate failure.  This change
	 *   fixes that.
	 */
	if (remove_files(tmpfile, map_addr) == FAILED) {
		free(empty_buf);
		return FAILED;
	}

	free(empty_buf);
	close(fd);
	return SUCCESS;
}

/******************************************************************************/
/*                                                                            */
/* Test:        Test case tests the race condition between simultaneous read  */
/*              faults in the same address space.                             */
/*                                                                            */
/* Description: map a file into memory, create threads and execute a thread   */
/*              function that will cause read faults by simultaneously reading*/
/*              from this memory space.                                       */
/******************************************************************************/
static int test1(void)
{
	tst_resm(TINFO, "test1: Test case tests the race condition between "
		 "simultaneous read faults in the same address space.");
	return map_and_thread("./tmp.file.1", thread_fault, READ_FAULT, NUMTHREAD);
}

/******************************************************************************/
/*                                                                            */
/* Test:        Test case tests the race condition between simultaneous write */
/*              faults in the same address space.                             */
/*                                                                            */
/* Description: map a file into memory, create threads and execute a thread   */
/*              function that will cause write faults by simultaneously       */
/*              writing to this memory space.                                 */
/******************************************************************************/
static int test2(void)
{
	tst_resm(TINFO, "test2: Test case tests the race condition between "
		 "simultaneous write faults in the same address space.");
	return map_and_thread("./tmp.file.2", thread_fault, WRITE_FAULT, NUMTHREAD);
}

/******************************************************************************/
/*                                                                            */
/* Test:        Test case tests the race condition between simultaneous COW   */
/*              faults in the same address space.                             */
/*                                                                            */
/* Description: map a file into memory, create threads and execute a thread   */
/*              function that will cause COW faults by simultaneously         */
/*              writing to this memory space.                                 */
/*                                                                            */
/******************************************************************************/
static int test3(void)
{
	tst_resm(TINFO, "test3: Test case tests the race condition between "
		 "simultaneous COW faults in the same address space.");
	return map_and_thread("./tmp.file.3", thread_fault, COW_FAULT, NUMTHREAD);
}

/******************************************************************************/
/*                                                                            */
/* Test:        Test case tests the race condition between simultaneous READ  */
/*              faults in the same address space. File mapped is /dev/zero    */
/*                                                                            */
/* Description: Map a file into memory, create threads and execute a thread   */
/*              function that will cause READ faults by simultaneously        */
/*              writing to this memory space.                                 */
/*                                                                            */
/******************************************************************************/
static int test4(void)
{
	tst_resm(TINFO, "test4: Test case tests the race condition between "
		 "simultaneous READ faults in the same address space. "
		 "The file mapped is /dev/zero");
	return map_and_thread("/dev/zero", thread_fault, COW_FAULT, NUMTHREAD);
}

/******************************************************************************/
/*                                                                            */
/* Test:    Test case tests the race condition between simultaneous           */
/*         fork - exit faults in the same address space.                      */
/*                                                                            */
/* Description: Initialize large data in the parent process, fork a child and */
/*              and the parent waits for the child to complete execution.     */
/*                                                                            */
/******************************************************************************/
static int test5(void)
{
	int fork_ndx = 0;
	pid_t pid = 0;
	int wait_status = 0;

	tst_resm(TINFO, "test5: Test case tests the race condition between "
		 "simultaneous fork - exit faults in the same address space.");

	/* increment the  program's  data  space  by 200*1024 (BRKSZ) bytes       */

	if (sbrk(BRKSZ) == (void *) - 1) {
		perror("test5(): sbrk()");
		fflush(NULL);
		return FAILED;
	}

	/* fork NUMTHREAD number of processes, assumption is on SMP each will get */
	/* a separate CPU if NRCPUS = NUMTHREAD. The child does nothing; exits    */
	/* immediately, parent waits for child to complete execution.             */
	do {
		if (!(pid = fork()))
			_exit(0);
		else {
			if (pid != -1)
				wait(&wait_status);
		}

	} while (fork_ndx++ < NUMTHREAD);

	if (sbrk(-BRKSZ) == (void *) - 1) {
		tst_resm(TINFO, "test5(): rollback sbrk failed");
		fflush(NULL);
		perror("test5(): sbrk()");
		fflush(NULL);
		return FAILED;
	}
	return SUCCESS;
}

/******************************************************************************/
/*                                                                            */
/* Test:        Test case tests the race condition between simultaneous       */
/*              fork - exec - exit faults in the same address space.          */
/*                                                                            */
/* Description: Initialize large data in the parent process, fork a child and */
/*              and the parent waits for the child to complete execution. The */
/*              child program execs a dummy program.                          */
/*                                                                            */
/******************************************************************************/
static int test6(void)
{
	int res = SUCCESS;
	int fork_ndx = 0;
	pid_t pid = 0;
	int wait_status;
	char *argv_init[2] = { "arg1", NULL };

	tst_resm(TINFO, "test6: Test case tests the race condition between "
		 "simultaneous fork -exec - exit faults in the same address space.");

	/* increment the  program's  data  space  by 200*1024 (BRKSZ) bytes       */
	if (sbrk(BRKSZ) == (void *) - 1) {
		perror("test6(): sbrk()");
		fflush(NULL);
		return FAILED;
	}

	/* fork NUMTHREAD number of processes, assumption is on SMP each will get */
	/* a separate CPU if NRCPUS = NUMTHREAD. The child execs a dummy program  */
	/*  and parent waits for child to complete execution.                     */
	do {
		if (!(pid = fork())) {
			if (execvp("mmstress_dummy", argv_init) == -1) {
				if (execvp("./mmstress_dummy", argv_init) == -1) {
					perror("test6(): execvp()");
					fflush(NULL);
					exit(99);
				}
			}
		} else {
			if (pid != -1)
				wait(&wait_status);

			if (WEXITSTATUS(wait_status) != 0)
				res = FAILED;
		}

	} while (fork_ndx++ < NUMTHREAD);

	if (sbrk(-BRKSZ) == (void *) - 1) {
		tst_resm(TINFO, "test6(): rollback sbrk failed");
		fflush(NULL);
		perror("test6(): sbrk()");
		fflush(NULL);
		return FAILED;
	}

	return res;
}

static int (*(test_ptr)[]) () = {test1, test2, test3, test4, test5, test6};

static void run_test(unsigned int i)
{
	int rc;

	rc = test_ptr[i]();

	if (rc == SUCCESS)
		tst_resm(TPASS, "TEST %d Passed", i + 1);
	else
		tst_resm(TFAIL, "TEST %d Failed", i + 1);

	if (alarm_fired)
		tst_exit();
}

int main(int argc, char **argv)
{
	static char *version_info = "mmstress V1.00 04/17/2001";
	int ch;
	unsigned int i;
	int test_num = 0;
	int test_time = 0;
	int run_once = TRUE;

	static struct signal_info {
		int signum;
		char *signame;
	} sig_info[] = {
		{SIGHUP, "SIGHUP"},
		{SIGINT, "SIGINT"},
		{SIGQUIT, "SIGQUIT"},
		{SIGABRT, "SIGABRT"},
		{SIGBUS, "SIGBUS"},
		{SIGSEGV, "SIGSEGV"},
		{SIGALRM, "SIGALRM"},
		{SIGUSR1, "SIGUSR1"},
		{SIGUSR2, "SIGUSR2"},
		{SIGENDSIG, "ENDSIG"}
	};

	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);

	if (argc < 2)
		tst_resm(TINFO, "run %s -h for all options", argv[0]);

	while ((ch = getopt(argc, argv, "hn:p:t:vV")) != -1) {
		switch (ch) {
		case 'h':
			usage(argv[0]);
			break;
		case 'n':
			test_num = atoi(optarg);
			break;
		case 'p':
			pages_num = atoi(optarg);
			break;
		case 't':
			tst_resm(TINFO,
				 "Test is scheduled to run for %d hours",
				 test_time = atoi(optarg));
			run_once = FALSE;
			break;
		case 'v':
			verbose_print = TRUE;
			break;
		case 'V':
			tst_resm(TINFO, "%s: %s", argv[0], version_info);
			break;
		case '?':
			fprintf(stderr,
				"%s: unknown option - %c ignored\n",
				argv[0], optopt);
			break;
		default:
			tst_brkm(TBROK, NULL, "%s: getopt() failed!!!",
				 argv[0]);
		}
	}

	set_timer(test_time);

	for (i = 0; sig_info[i].signum != -1; i++) {
		if (signal(sig_info[i].signum, sig_handler) == SIG_ERR) {
			tst_brkm(TBROK | TERRNO, NULL, "signal(%s) failed",
				 sig_info[i].signame);
		}
	}

	tst_tmpdir();

	do {
		if (!test_num) {
			for (i = 0; i < ARRAY_SIZE(test_ptr); i++)
				run_test(i);
		} else {
			if (test_num > (int)ARRAY_SIZE(test_ptr)) {
				tst_brkm(TBROK, NULL, "Invalid test number %i",
					 test_num);
			}

			run_test(test_num-1);
		}
	} while (!run_once);

	tst_rmdir();
	tst_exit();
}
