]> git.llucax.com Git - software/sercom.git/blobdiff - sercom/tester.py
Agregar cálculo de diffs.
[software/sercom.git] / sercom / tester.py
index da630d5d17b06624b31997e135d5c4b078de1cce..0d7f4981b9d6b8c9a638323aadacd1a850d7a2f8 100644 (file)
@@ -2,22 +2,40 @@
 
 from sercom.model import Entrega, CasoDePrueba
 from sercom.model import TareaFuente, TareaPrueba, ComandoFuente, ComandoPrueba
 
 from sercom.model import Entrega, CasoDePrueba
 from sercom.model import TareaFuente, TareaPrueba, 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')
 
 error_interno = _(u'\n**Error interno al preparar la entrega.**')
 
 import logging
 
 log = logging.getLogger('sercom.tester')
 
 error_interno = _(u'\n**Error interno al preparar la entrega.**')
 
-def check_call(*popenargs, **kwargs): #{{{ Python 2.5 forward-compatibility
+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]
+#}}}
+
+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
@@ -28,18 +46,19 @@ 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
 #}}}
 
 #{{{ Excepciones
 
 #}}}
 
 #{{{ Excepciones
 
-class CalledProcessError(Exception): #{{{ Python 2.5 forward-compatibility
+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."""
     """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."""
@@ -49,44 +68,41 @@ class CalledProcessError(Exception): #{{{ Python 2.5 forward-compatibility
     def __str__(self):
         return ("Command '%s' returned non-zero exit status %d"
             % (self.cmd, self.returncode))
     def __str__(self):
         return ("Command '%s' returned non-zero exit status %d"
             % (self.cmd, self.returncode))
+sp.CalledProcessError = CalledProcessError
 #}}}
 
 class Error(StandardError): pass
 
 #}}}
 
 class Error(StandardError): pass
 
-class ExecutionFailure(Error, RuntimeError): pass
+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
+#}}}
 
 class RsyncError(Error, EnvironmentError): pass
 
 #}}}
 
 
 class RsyncError(Error, EnvironmentError): pass
 
 #}}}
 
-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))
-#}}}
-
-def get_pwdgrp(unam, gnam): #{{{
-    def do(type, funcnam, funcid, name):
-        try:
-            id = funcnam(name)[2]
-        except:
-            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)
+            log.debug(_(u'Descomprimiendo archivo "%s" en "%s"'), f, dst)
+            file(dst, 'w').write(zfile.read(f))
+    zfile.close()
 #}}}
 
 class SecureProcess(object): #{{{
 #}}}
 
 class SecureProcess(object): #{{{
@@ -99,24 +115,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?
@@ -124,12 +154,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)
@@ -143,13 +167,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)'),
         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)
+            user_info.user, user_info.group, user_info.uid, user_info.gid)
+        os.setegid(user_info.gid)
+        os.seteuid(user_info.uid)
 
     @property
     def build_path(self):
 
     @property
     def build_path(self):
@@ -162,11 +183,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
@@ -194,10 +215,10 @@ class Tester(object): #{{{
             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'))
+                log.exception(_('Hubo una excepcion inesperada desconocida')) # FIXME encoding
             else:
                 entrega.correcta = True
                 log.debug(_(u'Entrega correcta: %s'), entrega)
             else:
                 entrega.correcta = True
                 log.debug(_(u'Entrega correcta: %s'), entrega)
@@ -214,12 +235,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:
             log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
         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)
+                user_info.user, user_info.group, user_info.uid, user_info.gid)
+            os.setegid(user_info.gid) # Mortal de nuevo
+            os.seteuid(user_info.uid)
         unzip(entrega.archivos, self.build_path)
 
     def clean_chroot(self, entrega):
         unzip(entrega.archivos, self.build_path)
 
     def clean_chroot(self, entrega):
@@ -255,13 +276,13 @@ def ejecutar_caso_de_prueba(self, path, entrega): #{{{
             for tarea in tareas:
                 tarea.ejecutar(path, prueba)
         except ExecutionFailure, e:
             for tarea in tareas:
                 tarea.ejecutar(path, prueba)
         except ExecutionFailure, e:
-            prueba.pasada = False
+            prueba.exito = False
             if self.rechazar_si_falla:
                 entrega.exito = False
             if self.terminar_si_falla:
             if self.rechazar_si_falla:
                 entrega.exito = False
             if self.terminar_si_falla:
-                raise ExecutionError(e.comando, e.tarea, prueba)
+                raise ExecutionFailure(e.comando, e.tarea, self)
         else:
         else:
-            prueba.pasada = True
+            prueba.exito = True
     finally:
         prueba.fin = datetime.now()
 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
     finally:
         prueba.fin = datetime.now()
 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
@@ -277,7 +298,7 @@ def ejecutar_tarea_fuente(self, path, entrega): #{{{
         if self.rechazar_si_falla:
             entrega.exito = False
         if self.terminar_si_falla:
         if self.rechazar_si_falla:
             entrega.exito = False
         if self.terminar_si_falla:
-            raise ExecutionError(e.comando, tarea)
+            raise ExecutionFailure(e.comando, self)
 TareaFuente.ejecutar = ejecutar_tarea_fuente
 #}}}
 
 TareaFuente.ejecutar = ejecutar_tarea_fuente
 #}}}
 
@@ -291,37 +312,194 @@ def ejecutar_tarea_prueba(self, path, prueba): #{{{
         if self.rechazar_si_falla:
             prueba.exito = False
         if self.terminar_si_falla:
         if self.rechazar_si_falla:
             prueba.exito = False
         if self.terminar_si_falla:
-            raise ExecutionError(e.comando, tarea)
+            raise ExecutionFailure(e.comando, self)
 TareaPrueba.ejecutar = ejecutar_tarea_prueba
 #}}}
 
 def ejecutar_comando_fuente(self, path, entrega): #{{{
     log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
         entrega.shortrepr())
 TareaPrueba.ejecutar = ejecutar_tarea_prueba
 #}}}
 
 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()
+    unzip(self.archivos_entrada, path, # TODO try/except
+        {self.STDIN: '/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id}) # TODO /var/run/sercom
+    options = dict(
+        close_fds=True,
+        shell=True,
+        preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build')
+    )
+    if os.path.exists('/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id): # TODO
+        options['stdin'] = file('/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id, 'r') # TODO
+    else:
+        options['preexec_fn'].close_stdin = True
+    a_guardar = set(self.archivos_a_guardar)
+    if self.archivos_a_comparar:
+        zip_a_comparar = ZipFile(StringIO(self.archivos_a_comparar), 'r')
+        a_comparar = set(zip_a_comparar.namelist())
+    else:
+        zip_a_comparar = None
+        a_comparar = frozenset()
+    a_usar = frozenset(a_guardar | a_comparar)
+    if self.STDOUTERR in a_usar:
+        options['stdout'] = file('/tmp/sercom.tester.%s.stdouterr'
+            % comando_ejecutado.id, 'w') #TODO /var/run/sercom?
+        options['stderr'] = sp.STDOUT
+    else:
+        if self.STDOUT in a_usar:
+            options['stdout'] = file('/tmp/sercom.tester.%s.stdout'
+                % comando_ejecutado.id, 'w') #TODO /run/lib/sercom?
+        else:
+            options['preexec_fn'].close_stdout = True
+        if self.STDERR in a_usar:
+            options['stderr'] = file('/tmp/sercom.tester.%s.stderr'
+                % comando_ejecutado.id, 'w') #TODO /var/run/sercom?
+        else:
+            options['preexec_fn'].close_stderr = True
+    log.debug(_(u'Ejecutando como root: %s'), self.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(self.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
+            log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
+                user_info.user, user_info.group, user_info.uid, user_info.gid)
+            os.setegid(user_info.gid) # Mortal de nuevo
+            os.seteuid(user_info.uid)
+    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
+    if self.retorno != self.RET_ANY:
+        if self.retorno == self.RET_FAIL:
+            if proc.returncode == 0:
+                if self.rechazar_si_falla:
+                    entrega.correcta = 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 self.retorno != proc.returncode:
+            if self.rechazar_si_falla:
+                entrega.correcta = 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') % (self.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'),
+                    self.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') \
+                    % (self.retorno, proc.returncode)
+                log.debug(_(u'Se esperaba terminar con un código de retorno '
+                    u'%s pero se obtuvo %s.\n'), self.retorno, proc.returncode)
+    if comando_ejecutado.exito is None:
+        log.debug(_(u'Código de retorno OK'))
     comando_ejecutado.fin = datetime.now()
     comando_ejecutado.fin = datetime.now()
+    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('/tmp/sercom.tester.%s.stdouterr'
+                % comando_ejecutado.id, self.STDOUTERR)
+        else:
+            if self.STDOUT in a_guardar:
+                a_guardar.remove(self.STDOUT)
+                zip.write('/tmp/sercom.tester.%s.stdout'
+                    % comando_ejecutado.id, self.STDOUT)
+            if self.STDERR in a_guardar:
+                a_guardar.remove(self.STDERR)
+                zip.write('/tmp/sercom.tester.%s.stderr'
+                    % 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.correcta = 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_guardados = buffer.getvalue()
+        file('/tmp/guardado.zip', 'w').write(buffer.getvalue()) # XXX TODO FIXME sacar
+    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.correcta = 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('/tmp/sercom.tester.%s.stdouterr' % 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('/tmp/sercom.tester.%s.stdout' % 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('/tmp/sercom.tester.%s.stderr' % 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.correcta = 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.archivos_guardados = buffer.getvalue()
+        file('/tmp/comparado.zip', 'w').write(buffer.getvalue()) # XXX TODO FIXME sacar
+
+
+    if comando_ejecutado.exito is None:
+        comando_ejecutado.exito = True
+    elif self.terminar_si_falla:
+        raise ExecutionFailure(self)
+
 #    if no_anda_ejecucion: # TODO
 #        comando_ejecutado.exito = False
 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
 #    if no_anda_ejecucion: # TODO
 #        comando_ejecutado.exito = False
 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
@@ -344,8 +522,6 @@ def ejecutar_comando_fuente(self, path, entrega): #{{{
 #    else:
 #        comando_ejecutado.exito = True
 #        comando_ejecutado.observaciones += 'xxx OK' # TODO
 #    else:
 #        comando_ejecutado.exito = True
 #        comando_ejecutado.observaciones += 'xxx OK' # TODO
-    comando_ejecutado.exito = True
-    comando_ejecutado.observaciones += 'xxx OK' # TODO
 ComandoFuente.ejecutar = ejecutar_comando_fuente
 #}}}
 
 ComandoFuente.ejecutar = ejecutar_comando_fuente
 #}}}