]> git.llucax.com Git - z.facultad/75.52/sercom.git/blobdiff - sercom/tester.py
Agregar finalizador de instancias de entrega.
[z.facultad/75.52/sercom.git] / sercom / tester.py
index a77c573bf4e040fa3ff89bc2941d824cd03ca4c1..e278417789cd39d28755eed51f126132214ec7f5 100644 (file)
@@ -1,33 +1,41 @@
 # vim: set et sw=4 sts=4 encoding=utf-8 foldmethod=marker:
 
 # vim: set et sw=4 sts=4 encoding=utf-8 foldmethod=marker:
 
-from sercom.model import Entrega, CasoDePrueba
-from sercom.model import TareaFuente, TareaPrueba, ComandoFuente, ComandoPrueba
+from sercom.model import Entrega, CasoDePrueba, Tarea, TareaFuente, TareaPrueba
+from sercom.model import ComandoFuente, ComandoPrueba
+from difflib import unified_diff, HtmlDiff
 from zipfile import ZipFile, BadZipfile
 from cStringIO import StringIO
 from shutil import rmtree
 from datetime import datetime
 from zipfile import ZipFile, BadZipfile
 from cStringIO import StringIO
 from shutil import rmtree
 from datetime import datetime
-from subprocess import Popen, PIPE, call #, check_call XXX Python 2.5
 from os.path import join
 from turbogears import config
 from os.path import join
 from turbogears import config
-import os, sys, pwd, grp
+import subprocess as sp
 import resource as rsrc
 import resource as rsrc
+import os, sys, pwd, grp
 import logging
 
 log = logging.getLogger('sercom.tester')
 
 import logging
 
 log = logging.getLogger('sercom.tester')
 
-class CalledProcessError(Exception): #{{{ Python 2.5 forward-compatibility
-    """This exception is raised when a process run by check_call() returns
-    a non-zero exit status.  The exit status will be stored in the
-    returncode attribute."""
-    def __init__(self, returncode, cmd):
-        self.returncode = returncode
-        self.cmd = cmd
-    def __str__(self):
-        return ("Command '%s' returned non-zero exit status %d"
-            % (self.cmd, self.returncode))
+error_interno = _(u'\n**Error interno al preparar la entrega.**')
+
+class UserInfo(object): #{{{
+    def __init__(self, user):
+        try:
+            info = pwd.getpwnam(user)
+        except:
+            info = pwd.get(int(user))
+        self.user = info[0]
+        self.uid = info[2]
+        self.gid = info[3]
+        self.name = info[4]
+        self.home = info[5]
+        self.shell = info[6]
+        self.group = grp.getgrgid(self.gid)[0]
 #}}}
 
 #}}}
 
-def check_call(*popenargs, **kwargs): #{{{ Python 2.5 forward-compatibility
+user_info = UserInfo(config.get('sercom.tester.user', 65534))
+
+def check_call(*popenargs, **kwargs): #{{{ XXX Python 2.5 forward-compatibility
     """Run command with arguments.  Wait for command to complete.  If
     the exit code was zero then return, otherwise raise
     CalledProcessError.  The CalledProcessError object will have the
     """Run command with arguments.  Wait for command to complete.  If
     the exit code was zero then return, otherwise raise
     CalledProcessError.  The CalledProcessError object will have the
@@ -38,51 +46,79 @@ def check_call(*popenargs, **kwargs): #{{{ Python 2.5 forward-compatibility
 
     check_call(["ls", "-l"])
     """
 
     check_call(["ls", "-l"])
     """
-    retcode = call(*popenargs, **kwargs)
+    retcode = sp.call(*popenargs, **kwargs)
     cmd = kwargs.get("args")
     if cmd is None:
         cmd = popenargs[0]
     if retcode:
     cmd = kwargs.get("args")
     if cmd is None:
         cmd = popenargs[0]
     if retcode:
-        raise CalledProcessError(retcode, cmd)
+        raise sp.CalledProcessError(retcode, cmd)
     return retcode
     return retcode
+sp.check_call = check_call
 #}}}
 
 #}}}
 
-class Error(StandardError): pass
+#{{{ Excepciones
 
 
-class ExecutionFailure(Error, RuntimeError): pass
+class CalledProcessError(Exception): #{{{ XXX Python 2.5 forward-compatibility
+    """This exception is raised when a process run by check_call() returns
+    a non-zero exit status.  The exit status will be stored in the
+    returncode attribute."""
+    def __init__(self, returncode, cmd):
+        self.returncode = returncode
+        self.cmd = cmd
+    def __str__(self):
+        return ("Command '%s' returned non-zero exit status %d"
+            % (self.cmd, self.returncode))
+sp.CalledProcessError = CalledProcessError
+#}}}
 
 
-class RsyncError(Error, EnvironmentError): pass
+class Error(StandardError): pass
 
 
-error_interno = _(u'\n**Error interno al preparar la entrega.**')
+class ExecutionFailure(Error, RuntimeError): #{{{
+    def __init__(self, comando, tarea=None, caso_de_prueba=None):
+        self.comando = comando
+        self.tarea = tarea
+        self.caso_de_prueba = caso_de_prueba
+#}}}
+
+#}}}
 
 
-def unzip(bytes, dst): # {{{
-    log.debug(_(u'Intentando descomprimir en %s'), dst)
+def unzip(bytes, default_dst='.', specific_dst=dict()): # {{{
+    u"""Descomprime un buffer de datos en formato ZIP.
+    Los archivos se descomprimen en default_dst a menos que exista una entrada
+    en specific_dst cuya clave sea el nombre de archivo a descomprimir, en
+    cuyo caso, se descomprime usando como destino el valor de dicha clave.
+    """
+    log.debug(_(u'Intentando descomprimir'))
     if bytes is None:
         return
     zfile = ZipFile(StringIO(bytes), 'r')
     for f in zfile.namelist():
     if bytes is None:
         return
     zfile = ZipFile(StringIO(bytes), 'r')
     for f in zfile.namelist():
+        dst = join(specific_dst.get(f, default_dst), f)
         if f.endswith(os.sep):
         if f.endswith(os.sep):
-            log.debug(_(u'Creando directorio %s'), f)
-            os.mkdir(join(dst, f))
+            log.debug(_(u'Creando directorio "%s" en "%s"'), f, dst)
+            os.mkdir(dst)
         else:
         else:
-            log.debug(_(u'Descomprimiendo archivo %s'), f)
-            file(join(dst, f), 'w').write(zfile.read(f))
+            log.debug(_(u'Descomprimiendo archivo "%s" en "%s"'), f, dst)
+            file(dst, 'w').write(zfile.read(f))
+    zfile.close()
 #}}}
 
 #}}}
 
-def get_pwdgrp(unam, gnam): #{{{
-    def do(type, funcnam, funcid, name):
-        try:
-            id = funcnam(name)[2]
-        except:
+class Multizip(object): #{{{
+    def __init__(self, *zip_streams):
+        self.zips = [ZipFile(StringIO(z), 'r') for z in zip_streams
+            if z is not None]
+        self.names = set()
+        for z in self.zips:
+            self.names |= set(z.namelist())
+    def read(self, name):
+        for z in self.zips:
             try:
             try:
-                id = int(name)
-                name = funcid(id)[0]
-            except Exception, e:
-                log.critical(_(u'No existe el %s %s (%s)'), type, name, e)
-                sys.exit(1)
-        return (id, name)
-    return do('usuario', pwd.getpwnam, pwd.getpwuid, unam) \
-        + do('grupo', grp.getgrnam, grp.getgrgid, gnam)
+                return z.read(name)
+            except KeyError:
+                pass
+        raise KeyError(name)
+    def namelist(self):
+        return self.names
 #}}}
 
 class SecureProcess(object): #{{{
 #}}}
 
 class SecureProcess(object): #{{{
@@ -95,24 +131,38 @@ class SecureProcess(object): #{{{
         max_locks_memoria   = 0,
     )
     uid = config.get('sercom.tester.chroot.user', 65534)
         max_locks_memoria   = 0,
     )
     uid = config.get('sercom.tester.chroot.user', 65534)
-    gid = config.get('sercom.tester.chroot.group', 65534)
     MB = 1048576
     # XXX probar! make de un solo archivo lleva nproc=100 y nofile=15
     MB = 1048576
     # XXX probar! make de un solo archivo lleva nproc=100 y nofile=15
-    def __init__(self, comando, chroot, cwd):
-            self.comando = comando
-            self.chroot = chroot
-            self.cwd = cwd
+    def __init__(self, comando, chroot, cwd, close_stdin=False,
+                 close_stdout=False, close_stderr=False):
+        self.comando = comando
+        self.chroot = chroot
+        self.cwd = cwd
+        self.close_stdin = close_stdin
+        self.close_stdout = close_stdout
+        self.close_stderr = close_stderr
+        log.debug(_(u'Proceso segurizado: chroot=%s, cwd=%s, user=%s, cpu=%s, '
+            u'as=%sMiB, fsize=%sMiB, nofile=%s, nproc=%s, memlock=%s'),
+            self.chroot, self.cwd, self.uid, self.max_tiempo_cpu,
+            self.max_memoria, self.max_tam_archivo, self.max_cant_archivos,
+            self.max_cant_procesos, self.max_locks_memoria)
     def __getattr__(self, name):
         if getattr(self.comando, name) is not None:
             return getattr(self.comando, name)
         return config.get('sercom.tester.limits.' + name, self.default[name])
     def __call__(self):
         x2 = lambda x: (x, x)
     def __getattr__(self, name):
         if getattr(self.comando, name) is not None:
             return getattr(self.comando, name)
         return config.get('sercom.tester.limits.' + name, self.default[name])
     def __call__(self):
         x2 = lambda x: (x, x)
+        if self.close_stdin:
+            os.close(0)
+        if self.close_stdout:
+            os.close(1)
+        if self.close_stderr:
+            os.close(2)
         os.chroot(self.chroot)
         os.chdir(self.cwd)
         os.chroot(self.chroot)
         os.chdir(self.cwd)
-        (uid, unam, gid, gnam) = get_pwdgrp(self.uid, self.gid)
-        os.setgid(gid)
-        os.setuid(uid)
+        uinfo = UserInfo(self.uid)
+        os.setgid(uinfo.gid)
+        os.setuid(uinfo.uid) # Somos mortales irreversiblemente
         rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
         rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
         rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
         rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
         rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
         rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
@@ -120,12 +170,6 @@ class SecureProcess(object): #{{{
         rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
         rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
         rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
         rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
         rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
         rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
-        log.debug('Proceso segurizado: chroot=%s, cwd=%s, user=%s(%s), '
-            'group=%s(%s), cpu=%s, as=%sMiB, fsize=%sMiB, nofile=%s, nproc=%s, '
-            'memlock=%s', self.chroot, self.cwd, unam, uid, gnam, gid,
-            self.max_tiempo_cpu, self.max_memoria, self.max_tam_archivo,
-            self.max_cant_archivos, self.max_cant_procesos,
-            self.max_locks_memoria)
         # Tratamos de forzar un sync para que entre al sleep del padre FIXME
         import time
         time.sleep(0)
         # Tratamos de forzar un sync para que entre al sleep del padre FIXME
         import time
         time.sleep(0)
@@ -139,13 +183,10 @@ class Tester(object): #{{{
         self.home = home
         self.queue = queue
         # Ahora somos mortales (oid mortales)
         self.home = home
         self.queue = queue
         # Ahora somos mortales (oid mortales)
-        euid = config.get('sercom.tester.user', 65534)
-        egid = config.get('sercom.tester.group', 65534)
-        (self.euid, self.eunam, self.egid, self.egnam) = get_pwdgrp(euid, egid)
-        log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
-            self.eunam, self.egnam, self.euid, self.egid)
-        os.setegid(self.egid)
-        os.seteuid(self.euid)
+        os.setegid(user_info.gid)
+        os.seteuid(user_info.uid)
+        log.debug(_(u'usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
+            user_info.user, user_info.group, user_info.uid, user_info.gid)
 
     @property
     def build_path(self):
 
     @property
     def build_path(self):
@@ -158,11 +199,11 @@ class Tester(object): #{{{
     @property
     def chroot(self):
         return join(self.path, 'chroot_' + self.name)
     @property
     def chroot(self):
         return join(self.path, 'chroot_' + self.name)
-    #}}}
 
     @property
     def orig_chroot(self):
         return join(self.path, 'chroot')
 
     @property
     def orig_chroot(self):
         return join(self.path, 'chroot')
+    #}}}
 
     def run(self): #{{{
         entrega_id = self.queue.get() # blocking
 
     def run(self): #{{{
         entrega_id = self.queue.get() # blocking
@@ -177,7 +218,7 @@ class Tester(object): #{{{
 
     def test(self, entrega): #{{{
         log.debug(_(u'Tester.test(entrega=%s)'), entrega)
 
     def test(self, entrega): #{{{
         log.debug(_(u'Tester.test(entrega=%s)'), entrega)
-        entrega.inicio_tareas = datetime.now()
+        entrega.inicio = datetime.now()
         try:
             try:
                 self.setup_chroot(entrega)
         try:
             try:
                 self.setup_chroot(entrega)
@@ -185,20 +226,22 @@ class Tester(object): #{{{
                 self.ejecutar_tareas_prueba(entrega)
                 self.clean_chroot(entrega)
             except ExecutionFailure, e:
                 self.ejecutar_tareas_prueba(entrega)
                 self.clean_chroot(entrega)
             except ExecutionFailure, e:
-                entrega.correcta = False
-                log.info(_(u'Entrega incorrecta: %s'), entrega)
+                pass
             except Exception, e:
                 if isinstance(e, SystemExit): raise
                 entrega.observaciones += error_interno
             except Exception, e:
                 if isinstance(e, SystemExit): raise
                 entrega.observaciones += error_interno
-                log.exception(_(u'Hubo una excepción inesperada: %s'), e)
+                log.exception(_('Hubo una excepcion inesperada')) # FIXME encoding
             except:
                 entrega.observaciones += error_interno
             except:
                 entrega.observaciones += error_interno
-                log.exception(_(u'Hubo una excepción inesperada desconocida'))
-            else:
-                entrega.correcta = True
-                log.debug(_(u'Entrega correcta: %s'), entrega)
+                log.exception(_('Hubo una excepcion inesperada desconocida')) # FIXME encoding
         finally:
         finally:
-            entrega.fin_tareas = datetime.now()
+            entrega.fin = datetime.now()
+            if entrega.exito is None:
+                entrega.exito = True
+            if entrega.exito:
+                log.info(_(u'Entrega correcta: %s'), entrega)
+            else:
+                log.info(_(u'Entrega incorrecta: %s'), entrega)
     #}}}
 
     def setup_chroot(self, entrega): #{{{ y clean_chroot()
     #}}}
 
     def setup_chroot(self, entrega): #{{{ y clean_chroot()
@@ -210,12 +253,12 @@ class Tester(object): #{{{
         os.seteuid(0) # Dios! (para chroot)
         os.setegid(0)
         try:
         os.seteuid(0) # Dios! (para chroot)
         os.setegid(0)
         try:
-            check_call(rsync)
+            sp.check_call(rsync)
         finally:
         finally:
-            log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
-                self.eunam, self.egnam, self.euid, self.egid)
-            os.setegid(self.egid) # Mortal de nuevo
-            os.seteuid(self.euid)
+            os.setegid(user_info.gid) # Mortal de nuevo
+            os.seteuid(user_info.uid)
+            log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
+                user_info.user, user_info.group, user_info.uid, user_info.gid)
         unzip(entrega.archivos, self.build_path)
 
     def clean_chroot(self, entrega):
         unzip(entrega.archivos, self.build_path)
 
     def clean_chroot(self, entrega):
@@ -245,137 +288,421 @@ def ejecutar_caso_de_prueba(self, path, entrega): #{{{
         entrega.shortrepr())
     tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
                 if isinstance(t, TareaPrueba)]
         entrega.shortrepr())
     tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
                 if isinstance(t, TareaPrueba)]
-    prueba = entrega.add_prueba(self)
+    prueba = entrega.add_prueba(self, inicio=datetime.now())
     try:
         try:
             for tarea in tareas:
                 tarea.ejecutar(path, prueba)
         except ExecutionFailure, e:
     try:
         try:
             for tarea in tareas:
                 tarea.ejecutar(path, prueba)
         except ExecutionFailure, e:
-            prueba.pasada = False
-            if self.rechazar_si_falla:
-                entrega.exito = False
-            if self.terminar_si_falla:
-                raise ExecutionError(e.comando, e.tarea, prueba)
-        else:
-            prueba.pasada = True
+            pass
     finally:
         prueba.fin = datetime.now()
     finally:
         prueba.fin = datetime.now()
+        if prueba.exito is None:
+            prueba.exito = True
+    if not prueba.exito and self.rechazar_si_falla:
+        entrega.exito = False
+    if not prueba.exito and self.terminar_si_falla:
+        raise ExecutionFailure(prueba)
 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
 #}}}
 
 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
 #}}}
 
-def ejecutar_tarea_fuente(self, path, entrega): #{{{
-    log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
-        entrega.shortrepr())
-    try:
-        for cmd in self.comandos:
-            cmd.ejecutar(path, entrega)
-    except ExecutionFailure, e:
-        if self.rechazar_si_falla:
-            entrega.exito = False
-        if self.terminar_si_falla:
-            raise ExecutionError(e.comando, tarea)
-TareaFuente.ejecutar = ejecutar_tarea_fuente
+def ejecutar_tarea(self, path, ejecucion): #{{{
+    log.debug(_(u'Tarea.ejecutar(path=%s, ejecucion=%s)'), path,
+        ejecucion.shortrepr())
+    for cmd in self.comandos:
+        cmd.ejecutar(path, ejecucion)
+Tarea.ejecutar = ejecutar_tarea
 #}}}
 
 #}}}
 
-def ejecutar_tarea_prueba(self, path, prueba): #{{{
-    log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
-        prueba.shortrepr())
-    try:
-        for cmd in self.comandos:
-            cmd.ejecutar(path, prueba)
-    except ExecutionFailure, e:
-        if self.rechazar_si_falla:
-            prueba.exito = False
-        if self.terminar_si_falla:
-            raise ExecutionError(e.comando, tarea)
-TareaPrueba.ejecutar = ejecutar_tarea_prueba
-#}}}
+# TODO generalizar ejecutar_comando_xxxx!!!
 
 def ejecutar_comando_fuente(self, path, entrega): #{{{
     log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
         entrega.shortrepr())
 
 def ejecutar_comando_fuente(self, path, entrega): #{{{
     log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
         entrega.shortrepr())
-    unzip(self.archivos_entrada, path) # TODO try/except
     comando_ejecutado = entrega.add_comando_ejecutado(self)
     comando_ejecutado = entrega.add_comando_ejecutado(self)
-    # Abro archivos para fds básicos (FIXME)
-    options = dict(close_fds=True, stdin=None, stdout=None, stderr=None,
-        preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build'))
-    log.debug(_(u'Ejecutando como root: %s'), ' '.join(self.comando))
-    uid = os.geteuid()
-    gid = os.getegid()
+    basetmp = '/tmp/sercom.tester.fuente' # FIXME TODO /var/run/sercom?
+    unzip(self.archivos_entrada, path, # TODO try/except
+        {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
+    options = dict(
+        close_fds=True,
+        shell=True,
+        preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build') #FIXME!!! path
+    )
+    if os.path.exists('%s.%s.stdin' % (basetmp, comando_ejecutado.id)):
+        options['stdin'] = file('%s.%s.stdin' % (basetmp, comando_ejecutado.id),
+            'r')
+    else:
+        options['preexec_fn'].close_stdin = True
+    a_guardar = set(self.archivos_a_guardar)
+    zip_a_comparar = Multizip(self.archivos_a_comparar)
+    a_comparar = set(zip_a_comparar.namelist())
+    a_usar = frozenset(a_guardar | a_comparar)
+    if self.STDOUTERR in a_usar:
+        options['stdout'] = file('%s.%s.stdouterr' % (basetmp,
+            comando_ejecutado.id), 'w')
+        options['stderr'] = sp.STDOUT
+    else:
+        if self.STDOUT in a_usar:
+            options['stdout'] = file('%s.%s.stdout' % (basetmp,
+                comando_ejecutado.id), 'w')
+        else:
+            options['preexec_fn'].close_stdout = True
+        if self.STDERR in a_usar:
+            options['stderr'] = file('%s.%s.stderr' % (basetmp,
+                comando_ejecutado.id), 'w')
+        else:
+            options['preexec_fn'].close_stderr = True
+    comando = self.comando # FIXME Acá tiene que diferenciarse de ComandoPrueba
+    comando_ejecutado.inicio = datetime.now()
+    log.debug(_(u'Ejecutando como root: %s'), comando)
     os.seteuid(0) # Dios! (para chroot)
     os.setegid(0)
     try:
         try:
     os.seteuid(0) # Dios! (para chroot)
     os.setegid(0)
     try:
         try:
-            proc = Popen(self.comando, **options)
+            proc = sp.Popen(comando, **options)
         finally:
         finally:
-            log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s'),
-                uid, gid)
-            os.setegid(gid) # Mortal de nuevo
-            os.seteuid(uid)
-    except Exception, e: # FIXME poner en el manejo de exceptiones estandar
+            os.setegid(user_info.gid) # Mortal de nuevo
+            os.seteuid(user_info.uid)
+            log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
+                user_info.user, user_info.group, user_info.uid, user_info.gid)
+    except Exception, e:
         if hasattr(e, 'child_traceback'):
             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
         raise
         if hasattr(e, 'child_traceback'):
             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
         raise
-    proc.wait()
+    proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
     comando_ejecutado.fin = datetime.now()
     comando_ejecutado.fin = datetime.now()
-#    if no_anda_ejecucion: # TODO
-#        comando_ejecutado.exito = False
-#        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
-#        if self.rechazar_si_falla:
-#            entrega.exito = False
-#        if self.terminar_si_falla: # TODO
-#            raise ExecutionFailure(self)
-    # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
-    # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
-    # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
-#    for archivo in self.archivos_salida:
-#        pass # TODO hacer diff
-#    if archivos_mal: # TODO
-#        comando_ejecutado.exito = False
-#        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
-#        if self.rechazar_si_falla:
-#            entrega.exito = False
-#        if self.terminar_si_falla: # TODO
-#            raise ExecutionFailure(self)
-#    else:
-#        comando_ejecutado.exito = True
-#        comando_ejecutado.observaciones += 'xxx OK' # TODO
-    comando_ejecutado.exito = True
-    comando_ejecutado.observaciones += 'xxx OK' # TODO
+    retorno = self.retorno
+    if retorno != self.RET_ANY:
+        if retorno == self.RET_FAIL:
+            if proc.returncode == 0:
+                if self.rechazar_si_falla:
+                    entrega.exito = False
+                comando_ejecutado.exito = False
+                comando_ejecutado.observaciones += _(u'Se esperaba que el '
+                    u'programa termine con un error (código de retorno '
+                    u'distinto de 0) pero terminó bien (código de retorno '
+                    u'0).\n')
+                log.debug(_(u'Se esperaba que el programa termine '
+                    u'con un error (código de retorno distinto de 0) pero '
+                    u'terminó bien (código de retorno 0).\n'))
+        elif retorno != proc.returncode:
+            if self.rechazar_si_falla:
+                entrega.exito = False
+            comando_ejecutado.exito = False
+            if proc.returncode < 0:
+                comando_ejecutado.observaciones += _(u'Se esperaba terminar '
+                    u'con un código de retorno %s pero se obtuvo una señal %s '
+                    u'(%s).\n') % (retorno, -proc.returncode, -proc.returncode) # TODO poner con texto
+                log.debug(_(u'Se esperaba terminar con un código '
+                    u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
+                    retorno, -proc.returncode, -proc.returncode)
+            else:
+                comando_ejecutado.observaciones += _(u'Se esperaba terminar '
+                    u'con un código de retorno %s pero se obtuvo %s.\n') \
+                    % (retorno, proc.returncode)
+                log.debug(_(u'Se esperaba terminar con un código de retorno '
+                    u'%s pero se obtuvo %s.\n'), retorno, proc.returncode)
+    if comando_ejecutado.exito is None:
+        log.debug(_(u'Código de retorno OK'))
+    if a_guardar:
+        buffer = StringIO()
+        zip = ZipFile(buffer, 'w')
+        # Guardamos stdout/stderr
+        if self.STDOUTERR in a_guardar:
+            a_guardar.remove(self.STDOUTERR)
+            zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
+                self.STDOUTERR)
+        else:
+            if self.STDOUT in a_guardar:
+                a_guardar.remove(self.STDOUT)
+                zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
+                    self.STDOUT)
+            if self.STDERR in a_guardar:
+                a_guardar.remove(self.STDERR)
+                zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
+                    self.STDERR)
+        # Guardamos otros
+        for f in a_guardar:
+            if not os.path.exists(join(path, f)):
+                if self.rechazar_si_falla:
+                    entrega.exito = False
+                comando_ejecutado.exito = False
+                comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
+                    u'"%s" para guardar pero no fue encontrado.\n') % f
+                log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
+                    u'no fue encontrado'), f)
+            else:
+                zip.write(join(path, f), f)
+        zip.close()
+        comando_ejecutado.archivos = buffer.getvalue()
+    def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
+             newname='entregado'):
+        if longname is None:
+            longname = name
+        new = file(new, 'r').readlines()
+        orig = zip_in.read(name).split('\n')
+        udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
+            tofile=name+'.'+newname)))
+        if udiff:
+            if self.rechazar_si_falla:
+                entrega.exito = False
+            comando_ejecutado.exito = False
+            comando_ejecutado.observaciones += _(u'%s no coincide con lo '
+                u'esperado (archivo "%s.diff").\n') % (longname, name)
+            log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
+                longname, name)
+            htmldiff = HtmlDiff().make_file(orig, new,
+                fromdesc=name+'.'+origname, todesc=name+'.'+newname,
+                context=True, numlines=3)
+            zip_out.writestr(name + '.diff', udiff)
+            zip_out.writestr(name + '.diff.html', htmldiff)
+            return True
+        else:
+            return False
+    if a_comparar:
+        buffer = StringIO()
+        zip = ZipFile(buffer, 'w')
+        # Comparamos stdout/stderr
+        if self.STDOUTERR in a_comparar:
+            a_comparar.remove(self.STDOUTERR)
+            diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
+                zip_a_comparar, zip, self.STDOUTERR,
+                _(u'La salida estándar y de error combinada'))
+        else:
+            if self.STDOUT in a_comparar:
+                a_comparar.remove(self.STDOUT)
+                diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
+                    zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
+            if self.STDERR in a_comparar:
+                a_comparar.remove(self.STDERR)
+                diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
+                    zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
+        # Comparamos otros
+        for f in a_comparar:
+            if not os.path.exists(join(path, f)):
+                if self.rechazar_si_falla:
+                    entrega.exito = False
+                comando_ejecutado.exito = False
+                comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
+                    u'"%s" para comparar pero no fue encontrado') % f
+                log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
+                    u'no fue encontrado'), f)
+            else:
+                diff(join(path, f), zip_a_comparar, zip, f)
+        zip.close()
+        comando_ejecutado.diferencias = buffer.getvalue()
+    if comando_ejecutado.exito is None:
+        comando_ejecutado.exito = True
+    elif self.terminar_si_falla:
+        raise ExecutionFailure(self)
+
 ComandoFuente.ejecutar = ejecutar_comando_fuente
 #}}}
 
 def ejecutar_comando_prueba(self, path, prueba): #{{{
 ComandoFuente.ejecutar = ejecutar_comando_fuente
 #}}}
 
 def ejecutar_comando_prueba(self, path, prueba): #{{{
+    # Diferencia con comando fuente: s/entrega/prueba/ y s/build/test/ en path
+    # y setup/clean de test.
     log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
         prueba.shortrepr())
     log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
         prueba.shortrepr())
-    rmtree(path)
-    os.mkdir(path)
-    unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
-    unzip(self.archivos_entrada, path) # TODO try/except
+    caso_de_prueba = prueba.caso_de_prueba
     comando_ejecutado = prueba.add_comando_ejecutado(self)
     comando_ejecutado = prueba.add_comando_ejecutado(self)
-    # TODO ejecutar en chroot (path)
+    basetmp = '/tmp/sercom.tester.prueba' # FIXME TODO /var/run/sercom?
+    #{{{ Código que solo va en ComandoPrueba (setup de directorio)
+    rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
+        '--archive', '--acls', '--delete-during', '--force', # TODO config
+        'var/chroot_pepe/home/sercom/build/', path) # FIXME!!!! path
+    log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
+    os.seteuid(0) # Dios! (para chroot)
+    os.setegid(0)
+    try:
+        sp.check_call(rsync)
+    finally:
+        os.setegid(user_info.gid) # Mortal de nuevo
+        os.seteuid(user_info.uid)
+        log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
+            user_info.user, user_info.group, user_info.uid, user_info.gid)
+    #}}}
+    unzip(self.archivos_entrada, path, # TODO try/except
+        {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
+    unzip(caso_de_prueba.archivos_entrada, path, # TODO try/except     # FIXME Esto es propio de ComandoPrueba
+        {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)}) # FIXME Esto es propio de ComandoPrueba
+    options = dict(
+        close_fds=True,
+        shell=True,
+        preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/test') # FIXME!!!! path
+    )
+    if os.path.exists('%s.%s.stdin' % (basetmp, comando_ejecutado.id)):
+        options['stdin'] = file('%s.%s.stdin' % (basetmp, comando_ejecutado.id),
+            'r')
+    else:
+        options['preexec_fn'].close_stdin = True
+    a_guardar = set(self.archivos_a_guardar)
+    a_guardar |= set(caso_de_prueba.archivos_a_guardar)           # FIXME Esto es propio de ComandoPrueba
+    zip_a_comparar = Multizip(caso_de_prueba.archivos_a_comparar, # FIXME Esto es propio de ComandoPrueba
+        self.archivos_a_comparar)                                 # FIXME Esto es propio de ComandoPrueba
+    a_comparar = set(zip_a_comparar.namelist())
+    a_usar = frozenset(a_guardar | a_comparar)
+    if self.STDOUTERR in a_usar:
+        options['stdout'] = file('%s.%s.stdouterr' % (basetmp,
+            comando_ejecutado.id), 'w')
+        options['stderr'] = sp.STDOUT
+    else:
+        if self.STDOUT in a_usar:
+            options['stdout'] = file('%s.%s.stdout' % (basetmp,
+                comando_ejecutado.id), 'w')
+        else:
+            options['preexec_fn'].close_stdout = True
+        if self.STDERR in a_usar:
+            options['stderr'] = file('%s.%s.stderr' % (basetmp,
+                comando_ejecutado.id), 'w')
+        else:
+            options['preexec_fn'].close_stderr = True
+    comando = self.comando + ' ' + caso_de_prueba.comando # FIXME Esto es propio de ComandoPrueba
+    comando_ejecutado.inicio = datetime.now()
+    log.debug(_(u'Ejecutando como root: %s'), comando)
+    os.seteuid(0) # Dios! (para chroot)
+    os.setegid(0)
+    try:
+        try:
+            proc = sp.Popen(comando, **options)
+        finally:
+            os.setegid(user_info.gid) # Mortal de nuevo
+            os.seteuid(user_info.uid)
+            log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
+                user_info.user, user_info.group, user_info.uid, user_info.gid)
+    except Exception, e:
+        if hasattr(e, 'child_traceback'):
+            log.error(_(u'Error en el hijo: %s'), e.child_traceback)
+        raise
+    proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
     comando_ejecutado.fin = datetime.now()
     comando_ejecutado.fin = datetime.now()
-#    if no_anda_ejecucion: # TODO
-#        comando_ejecutado.exito = False
-#        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
-#        if self.rechazar_si_falla:
-#            entrega.exito = False
-#        if self.terminar_si_falla: # TODO
-#            raise ExecutionFailure(self) # TODO info de error
-#    for archivo in self.archivos_salida:
-#        pass # TODO hacer diff
-#    if archivos_mal: # TODO
-#        comando_ejecutado.exito = False
-#        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
-#        if self.rechazar_si_falla:
-#            entrega.exito = False
-#        if self.terminar_si_falla: # TODO
-#            raise ExecutionFailure(comando=self) # TODO info de error
-#    else:
-#        comando_ejecutado.exito = True
-#        comando_ejecutado.observaciones += 'xxx OK' # TODO
-    comando_ejecutado.exito = True
-    comando_ejecutado.observaciones += 'xxx OK' # TODO
+    retorno = self.retorno
+    if retorno == self.RET_PRUEBA:                # FIXME Esto es propio de ComandoPrueba
+        retorno = caso_de_prueba.retorno   # FIXME Esto es propio de ComandoPrueba
+    if retorno != self.RET_ANY:
+        if retorno == self.RET_FAIL:
+            if proc.returncode == 0:
+                if self.rechazar_si_falla:
+                    prueba.exito = False
+                comando_ejecutado.exito = False
+                comando_ejecutado.observaciones += _(u'Se esperaba que el '
+                    u'programa termine con un error (código de retorno '
+                    u'distinto de 0) pero terminó bien (código de retorno '
+                    u'0).\n')
+                log.debug(_(u'Se esperaba que el programa termine '
+                    u'con un error (código de retorno distinto de 0) pero '
+                    u'terminó bien (código de retorno 0).\n'))
+        elif retorno != proc.returncode:
+            if self.rechazar_si_falla:
+                prueba.exito = False
+            comando_ejecutado.exito = False
+            if proc.returncode < 0:
+                comando_ejecutado.observaciones += _(u'Se esperaba terminar '
+                    u'con un código de retorno %s pero se obtuvo una señal %s '
+                    u'(%s).\n') % (retorno, -proc.returncode, -proc.returncode) # TODO poner con texto
+                log.debug(_(u'Se esperaba terminar con un código '
+                    u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
+                    retorno, -proc.returncode, -proc.returncode)
+            else:
+                comando_ejecutado.observaciones += _(u'Se esperaba terminar '
+                    u'con un código de retorno %s pero se obtuvo %s.\n') \
+                    % (retorno, proc.returncode)
+                log.debug(_(u'Se esperaba terminar con un código de retorno '
+                    u'%s pero se obtuvo %s.\n'), retorno, proc.returncode)
+    if comando_ejecutado.exito is None:
+        log.debug(_(u'Código de retorno OK'))
+    if a_guardar:
+        buffer = StringIO()
+        zip = ZipFile(buffer, 'w')
+        # Guardamos stdout/stderr
+        if self.STDOUTERR in a_guardar:
+            a_guardar.remove(self.STDOUTERR)
+            zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
+                self.STDOUTERR)
+        else:
+            if self.STDOUT in a_guardar:
+                a_guardar.remove(self.STDOUT)
+                zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
+                    self.STDOUT)
+            if self.STDERR in a_guardar:
+                a_guardar.remove(self.STDERR)
+                zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
+                    self.STDERR)
+        # Guardamos otros
+        for f in a_guardar:
+            if not os.path.exists(join(path, f)):
+                if self.rechazar_si_falla:
+                    prueba.exito = False
+                comando_ejecutado.exito = False
+                comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
+                    u'"%s" para guardar pero no fue encontrado.\n') % f
+                log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
+                    u'no fue encontrado'), f)
+            else:
+                zip.write(join(path, f), f)
+        zip.close()
+        comando_ejecutado.archivos = buffer.getvalue()
+    def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
+             newname='entregado'):
+        if longname is None:
+            longname = name
+        new = file(new, 'r').readlines()
+        orig = zip_in.read(name).split('\n')
+        udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
+            tofile=name+'.'+newname)))
+        if udiff:
+            if self.rechazar_si_falla:
+                prueba.exito = False
+            comando_ejecutado.exito = False
+            comando_ejecutado.observaciones += _(u'%s no coincide con lo '
+                u'esperado (archivo "%s.diff").\n') % (longname, name)
+            log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
+                longname, name)
+            htmldiff = HtmlDiff().make_file(orig, new,
+                fromdesc=name+'.'+origname, todesc=name+'.'+newname,
+                context=True, numlines=3)
+            zip_out.writestr(name + '.diff', udiff)
+            zip_out.writestr(name + '.diff.html', htmldiff)
+            return True
+        else:
+            return False
+    if a_comparar:
+        buffer = StringIO()
+        zip = ZipFile(buffer, 'w')
+        # Comparamos stdout/stderr
+        if self.STDOUTERR in a_comparar:
+            a_comparar.remove(self.STDOUTERR)
+            diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
+                zip_a_comparar, zip, self.STDOUTERR,
+                _(u'La salida estándar y de error combinada'))
+        else:
+            if self.STDOUT in a_comparar:
+                a_comparar.remove(self.STDOUT)
+                diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
+                    zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
+            if self.STDERR in a_comparar:
+                a_comparar.remove(self.STDERR)
+                diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
+                    zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
+        # Comparamos otros
+        for f in a_comparar:
+            if not os.path.exists(join(path, f)):
+                if self.rechazar_si_falla:
+                    prueba.exito = False
+                comando_ejecutado.exito = False
+                comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
+                    u'"%s" para comparar pero no fue encontrado') % f
+                log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
+                    u'no fue encontrado'), f)
+            else:
+                diff(join(path, f), zip_a_comparar, zip, f)
+        zip.close()
+        comando_ejecutado.diferencias = buffer.getvalue()
+    if comando_ejecutado.exito is None:
+        comando_ejecutado.exito = True
+    elif self.terminar_si_falla:
+        raise ExecutionFailure(self)
+
 ComandoPrueba.ejecutar = ejecutar_comando_prueba
 #}}}
 
 ComandoPrueba.ejecutar = ejecutar_comando_prueba
 #}}}