Commit af8f95f3 authored by Guillaume Huard's avatar Guillaume Huard
Browse files

Reworked the IOlimits part (now its named outputs limit)

parent 77ffe3bf
......@@ -181,19 +181,27 @@ notre programme
En outre, l'exécution elle même, lorsqu'il s'agit du code soumis par l'étudiant, est controllée par
deux réglages évitant une trop grande utilisation des ressources disponibles :
- =timeout= : valeur en secondes déterminant le temps alloué au test pour son exécution (10 par
défaut) ;
défaut). Si le test dépasse le temps imparti, il est tué par le signal 9 (SIGKILL) ;
- =IOlimit= : taille maximale en blocs d'un fichier créé par le test (100 blocs par défaut). La taille du
bloc dépend du système mais est souvent de 512 octets.
L'exemple suivant comprend une petite série de tests qui ne passe que si le programme s'exécute dans
les limites imposées :
bloc dépend du mode de bash elle est de 512 ou 1024 octets. Si un processus associé au test écrit
plus que cette limite dans un fichier, il reçoit le signal 25 (SIGXFSZ) qui, par défaut, met fin au processus en
question. Attention, cela ne met pas forcément fin au test : typiquement si le programme soumis
par l'étudiant est un script shell, les commandes sont exécutées dans des processus distincts et
si l'une d'elles se termine par le signal 25 ce n'est pas forcément le cas du script soumis
(comportement par défaut, qu'il est souhaitable de conserver dans un contexte d'apprentissage de
la programmation shell).
L'exemple suivant comprend une petite série de tests avec limites, qui passe pour tout programme
s'exécutant en moins de 2 secondes et ayant un code de retour nul. En outre, pour les tests 2 et 3,
si le programme ne pourra pas créer de fichier de plus de 1 bloc :
#+BEGIN_SRC perl :exports code
{
"limits.sh" => {
tests => {
test_1 => {}, # limites par défaut
test_2 => { timeout => 2, IOlimit => 100 },
test_3 => { timeout => 30, IOlimit => 1 }
}
"limites.sh" => {
code => 0, signal => 0,
tests => {
test_1 => { timeout => 2 },
test_2 => { IOlimit => 1 },
test_3 => { IOlimit => 1, timeout => 2 }
}
}
}
#+END_SRC
......
......@@ -2,11 +2,7 @@
use strict;
use File::Slurp;
use Data::Dumper;
use File::Temp qw(tempfile);
use IPC::Open3;
use Symbol 'gensym';
use Cwd;
use lib cwd;
use Errno;
our %mode;
our %colors = (
......@@ -84,21 +80,30 @@ sub get_value($$$) {
}
}
# Reads all the data available from a given fd and closes it
sub read_from_fd($) {
my $fd = shift;
error("Undefined fd in read_from_fd") unless defined($fd);
# Reads a file up to a given limit, -1 if no limit
sub bounded_read_file($$) {
my $name = shift;
my $bound = shift;
my $size = ($bound == -1) ? 1024 : $bound;
open(my $fh, "<", $name) || error("Cannot read $name");
my $data;
my $offset = 0;
while ((my $number = read ($fd, $data, 1024, $offset)) != 0) {
$offset += $number;
while ((my $number = read($fh, $data, $size, $offset)) != 0) {
if (defined($number)) {
$size -= $number if $bound != -1;
$offset += $number;
} else {
if (! $!{EINTR}) {
error("Error $! when reading");
}
}
}
close($fd);
return $data;
close($fh);
return ($data, $size == 0);
}
# Runs a command associated to a test with a given input, returns a hash that holds the content of standard output and error as well as exit code
# Looks at IOlimit, timeout, debug, args and input in the test to find out how to run it (but they are not necessarily expected to exist)
# Looks at outputs_limit, timeout, debug, args and input in the test to find out how to run it (but they are not necessarily expected to exist)
sub run_command($$$$) {
my $test_name = shift;
my $command = shift;
......@@ -117,22 +122,15 @@ sub run_command($$$$) {
my $data = {};
my $input = get_value($test, 'input', "");
write_file(".internal_input.txt", $input) || error("Cannot write input file");
my $outputs_limit = -1;
my $command_line = "$command$arguments >.internal_output.txt 2>.internal_error.txt <.internal_input.txt";
debug("Executing $command");
if ($has_limits) {
my $IOlimit .= get_value($test, 'IOlimit', 100);
my $timeout .= get_value($test, 'timeout', 10);
debug(" with IOlimit $IOlimit and timeout $timeout");
# We build an execution wrapper to ensure the use of bash instead of /bin/sh (system's default)
# This avoids issues with ulimit
my $command_script = "#!/bin/bash\n";
$command_script .= "ulimit -f$IOlimit\n";
$command_script .= "exec timeout -s9 $timeout ";
$command_script .= $command_line;
# debug("Command script :\n$command_script\n");
ensure_write(".execution_wrapper", $command_script);
chmod(0755, ".execution_wrapper") || error("Cannot make execution_wrapper executable");
$command_line = "./.execution_wrapper";
$outputs_limit = get_value($test, 'outputs_limit', 100);
my $timeout = get_value($test, 'timeout', 10);
debug(" with outputs_limit ${outputs_limit}Kb and timeout ${timeout}s");
$outputs_limit *= 1024 if $outputs_limit != -1;
$command_line = "exec timeout -s9 $timeout $command_line";
}
debug("\n");
# debug("Executing: $command_line\n");
......@@ -140,10 +138,9 @@ sub run_command($$$$) {
$data->{failure} = system($command_line);
$data->{signal} = $data->{failure} & 0xFF;
$data->{code} = $data->{failure} >> 8;
$data->{output} = read_file(".internal_output.txt");
$data->{error} = read_file(".internal_error.txt");
($data->{output}, $data->{output_bounded}) = bounded_read_file(".internal_output.txt", $outputs_limit);
($data->{error}, $data->{error_bounded}) = bounded_read_file(".internal_error.txt", $outputs_limit);
# debug("Result : ".Dumper($data));
remove(".execution_wrapper") if $has_limits;
remove(".internal_input.txt", ".internal_output.txt", ".internal_error.txt");
return $data;
}
......@@ -228,7 +225,7 @@ sub run_special($$$$$) {
$special->{compiled} = 1;
}
# Adds limits if any
if (exists($special->{IOlimit}) || exists($special->{timeout})) {
if (exists($special->{outputs_limit}) || exists($special->{timeout})) {
$has_limits = 1;
}
my $result;
......@@ -423,9 +420,13 @@ sub perform_tests($$) {
code => "returned the code",
signal => "received the signal" };
if (defined($test->{$part}) || ($data->{$part})) {
$details .= "Your program $description->{$part}:\n".preformat($data->{$part});
if ($data->{${part}.'_bounded'}) {
$details .= "Your program standard $part has reached the upper limit";
} else {
$details .= "Your program $description->{$part}:\n".preformat($data->{$part});
}
if (defined($test->{$part})) {
my $local_result = $data->{$part} eq $test->{$part};
my $local_result = ($data->{$part} eq $test->{$part}) && !$data->{${part}.'_bounded'};
if (!$local_result) {
$details .= "\nBut the expectation was:\n".preformat($test->{$part})."\n";
} else {
......@@ -437,12 +438,8 @@ sub perform_tests($$) {
} else {
$details .= "\n";
}
if (!$result && ($part eq 'signal')) {
if ($data->{signal} == 9) {
$details .= "Looks like a timeout, did you write an infinite loop ?\n"
} elsif ($data->{signal} == 25) {
$details .= "Filesize limit exceeded, did you write an infinite loop ?\n";
}
if (!$result && ($part eq 'signal') && ($data->{signal} == 9)) {
$details .= "Looks like a timeout, did you write an infinite loop ?\n";
}
}
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment