/* Driver program for checking IOI-like programs 1.11 Copyright (C) 2001-2007 Paolo Boldi and Sebastiano Vigna 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, 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. This program takes as input an executable program name, and tests the given program with respect to a number of test cases. Note that to perform the test, unless stdio is used each input file will be in turn hard-linked to the filename required by the program. Thus, it is wise to write-protect input files. If you do not use a comparator, the program does not need any external file/device or program. Thus, it can be compiled statically and safely used in a chroot'd environment. If you use a comparator, /bin/sh must be a shell (statically linked, if you plan to chroot) that will be used by popen(). You can use, for instance, sash. TEST CASES ========== Each test case is defined by one or more input files, and by one output file. The names of the input files and output file are provided by means of filename formats: each format is a filename pattern, with possibly one "%d" in it, that is substituted with 0, 1, 2 etc. to obtain the filenames of the input/output files. For example, suppose that the following input/output filename patterns are specified: - input filename patterns: ROAD%d.IN, CAST.IN, TEST%d - output filename pattern: OUT%d.OUT Then the first test case is given by the input files ROAD0.IN, CAST.IN, TEST0 and by the output file OUT0.OUT; the second test case is given by the input files ROAD1.IN, CAST.IN, TEST1 and by the output file OUT1.OUT; and so on. OPTION -i The option -i can be used to specify an input filename pattern. If more input filename patterns are needed, the option must be specified many times. If no -i option is present, then it is assumed that the only input filename pattern is input%d.txt. OPTION -o The option -o can be used to specify the output filename pattern; this option can appear at most once. If no -o option is present, then it is assumed that the only output filename pattern is output%d.txt. OPTION -n The option -n can be used to specify the number of test cases; test cases are numbered from 0 to the number of test cases minus 1. If no -n option is present, the number of test cases is assumed to be 10. PERFORMING THE TEST =================== The standard behaviour of the driver is as follows. For test case number k, the input files of the test case are copied onto files whose name is obtained by dropping the "%d" specification from the input filename pattern. Then the executable is run; it is expected to produce an output file whose name is obtained by dropping the "%d" specification from the output file pattern. In the example above, at the first test case, the files ROAD0.IN, CAST.IN, TEST0 are copied onto ROAD.IN, CAST.IN, TEST (respectively) and the executable is run. It should produce a file named OUT.OUT. OPTION -s This option can be specified only if there is exactly one input filename pattern. If this option is specified, the executable is expected to read from its standard input and to produce output on its standard output. Thus, the driver will feed the executable with the input file corresponding to the current test case, and will copy the standard output of the executable onto the required file. OPTION -t This option specifies the number of seconds of user time the executable is allowed to run (per test case); if the executable exceeds the specified amount of time, the score for that test case is assumed to be 0. The amount of time may be a float, but it is senseless to specify values below getrusage() accuracy. Note that the program will be actually killed after this number of seconds (rounded up), unless the -T option is specified. This might be a problem for programs performing a large amount of I/O, as that time is not added to the user time, but it could be considered by the operating system in calculating the timeout. If you plan to check programs of this kind, please set the -T option to a higher number of seconds. Default: 10. OPTION -T This option specifies the number of seconds after which the program will be killed. Since this time might include the time spent during I/O, sometime it is wise to set it higher than -t (it causes no harm, in any case). Default: the value of the -t option rounded up. OPTION -k OPTION -m This option specifies the amount of memory the executable is allowed to use (per test case); if the executable uses more than that amount, it is killed and the score for that test case is assumed to be 0. Currently does not work under Linux. Default: none. EVALUATING THE RESULT ===================== After the executable has been run, the output obtained should be checked for correctness. If no comparison program is specified (see below), the driver simply compares the output file obtained with the reference output file (for that test case); the comparison ignores the amount of leading and trailing whitespace in each line, and empty lines at the end of the files. If the two files are equal, the score assigned for that test case is 1; otherwise, it is 0. If a comparison program is specified, the comparison program is run. The comparison program receives the following arguments: - a list of one or more input filenames - the filename of the reference output - the filename of the output obtained. The comparison program can write log messages on its standard error, but it is expected to write on its own standard output one single floating-point number between 0 and 1: this number is the score that is assigned to the test case. OPTION -c This option is used to specify that a comparison program should be used. DRIVER OUTPUT ============= The driver outputs the score obtained on each test case, and the final total score (obtained by adding up the scores assigned to each test case). OPTION -r If this option is specified, the output file obtained at each test case is copied on the specified file. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define MAXINPUTFILES 16 #define BUFSIZE (1024*1024) char buffer1[BUFSIZE]; char buffer2[BUFSIZE]; /* For built-in file comparison */ char *compname; /* The name of the comparator, if any */ int ninputfiles = 0; char format[] = "%d", /* The format part */ *inputfile[MAXINPUTFILES], /* The vector of ninputfiles input files (possibly containing the format string) */ *outputfile = "output%d.txt", /* The output file */ *stdoutputfile, /* The output file without %d (i.e., what the program to be tested is expected to output) */ *resultfile; /* An optional result file format for storing the output of each test. */ /* Copies the input files that contain format onto their i-instantiation. Returns -1 in case of I/O error. */ int copy(int i) { char b[1024], c[1024], *p; int k; for(k=0; k ... Then, the unique float printed by the comparison program on its standard output is returned. The input files are obtained instantiating inputfile[]; the reference files are obtained instantiating outputfile; and the user output file is stdoutputfile (which is outputfile without format). */ float compare(int i) { FILE *f1,*f2; char *p1, *p2; int error, c; char filename[1024]; if ( compname ) { int k; FILE *f; char b[1024]; float result; sprintf(b, "%s ", compname); for(k=0; k\t\t\tUse this comparator\n" " -n \t\t\tNumber of test cases (default: 10)\n" " -t \t\t\tUser time limit in seconds (may be a float; default: 10)\n" " -T \t\t\tTimeout before signalling the process (may include system time; default: ceiled user time limit)\n" " -m \t\t\tMemory size limit in MiB=2^20B (default: none)\n" " -k \t\t\tMemory size limit in KiB=2^10B (default: none)\n" " -i \tDefault: input%%d.txt (more than one allowed)\n" " -o \tDefault: output%%d.txt\n" " -r \tDefault: none\n" " -s\t\t\t\tExecutable uses stdio\n" " -v\t\t\t\tVerbose (diagnostic) output\n" "\n" "Note that not all systems implement memory usage limits.\n" , argv[0]); return 1; } while( ( opt = getopt(argc, argv, "i:o:c:C:t:T:k:m:r:svn:") ) != -1 ) { switch(opt) { case 's': stdio = 1; break; case 'v': verbose = 1; break; case 't': if ((millisecs = (int)(atof(optarg)*1000)) <= 0) error("The number of seconds must be a positive number."); break; case 'T': if ((timeout = atoi(optarg)) <= 0) error("The timeout must be a positive integer."); break; case 'm': if (mem || (mem = atoi(optarg)) <= 0) error(mem <= 0 ? "The number of MiB must be a positive integer." : "Options -k and -m are not compatible"); mem*=1024; break; case 'k': if (mem || (mem = atoi(optarg)) <= 0) error(mem <= 0 ? "The number of KiB must be a positive integer." : "Options -k and -m are not compatible"); break; case 'n': if ((n = atoi(optarg)) <= 0) error("The number of test cases must be a positive integer."); break; case 'c': compname = optarg; break; case 'i': if (ninputfiles < MAXINPUTFILES) inputfile[ninputfiles++] = optarg; else error("Too many input files."); break; case 'o': outputfile = optarg; break; case 'r': resultfile = optarg; break; case ':': case '?': return 1; } } if (!timeout) timeout = ceil(millisecs/1000.0); mem *= 1024; if (stdio && ninputfiles > 1) error("You cannot redirect from more than one file."); if (ninputfiles == 0) inputfile[ninputfiles++] = "input%d.txt"; stdoutputfile = malloc(strlen(outputfile)); if (p = strstr(outputfile, format)) { strncpy(stdoutputfile, outputfile, p-outputfile); strcpy(stdoutputfile + (p-outputfile), p+strlen(format)); } else strcat(stdoutputfile, outputfile); if (optind >= argc) { fprintf(orig_stderr, "No executable specified.\n"); exit(1); } signal(SIGPIPE, SIG_IGN); // Ignore signals from broken pipes. strcpy(programname, argv[optind]); dummy[0] = programname; for (i=0;i millisecs) ) { if (WTERMSIG(childstatus) == SIGKILL) fprintf(orig_stderr, "Time out%s [user time: %6.3fs]\n", mem?" or allowed memory exceeded":"", curms/1000.0); /* Killed by the kernel. */ else if (curms > millisecs) fprintf(orig_stderr, "User time exceeded [%6.3fs]\n", curms/1000.0); /* Exceeded user time. */ else if (WTERMSIG(childstatus) == SIGQUIT) exit(1); /* Kill by ourselves--big problem. */ else fprintf(orig_stderr, "Execution error (signal %d)!\n", WTERMSIG(childstatus)); /* SIGSEGV, etc. */ continue; } fprintf(orig_stderr, "[user time: %6.3fs] ", curms/1000.0); if ( stat( stdoutputfile, &s ) ) { /* No output file */ fprintf(orig_stderr, "No output file\n"); } else { if ((cr = compare(i)) > 0) { fprintf(orig_stderr, "Success! (%.4f)\n", cr); correct += cr; } else { fprintf(orig_stderr, "Output file is not correct\n"); } if (resultfile) { /* If required, record output file. */ sprintf(b, resultfile, i); if (rename(stdoutputfile, b)) { perror("Cannot rename output file"); exit(1); } } } } } fprintf(orig_stderr, "Score: %f\n", correct); /* We now unlink everything we left behind. */ unlink(stdoutputfile); for(i=0; i