]> git.llucax.com Git - software/sercom.git/blob - sercom/tester.py
No guardar zip de diferencias si no hay ningún diff.
[software/sercom.git] / sercom / tester.py
1 # vim: set et sw=4 sts=4 encoding=utf-8 foldmethod=marker:
2
3 from sercom.model import Entrega, CasoDePrueba, Tarea, TareaFuente, TareaPrueba
4 from sercom.model import ComandoFuente, ComandoPrueba
5 from difflib import unified_diff, HtmlDiff
6 from zipfile import ZipFile, BadZipfile
7 from cStringIO import StringIO
8 from shutil import rmtree
9 from datetime import datetime
10 from os.path import join
11 from turbogears import config
12 import subprocess as sp
13 import resource as rsrc
14 import os, sys, pwd, grp
15 import logging
16
17 log = logging.getLogger('sercom.tester')
18
19 error_interno = _(u'\n**Error interno al preparar la entrega.**')
20
21 class UserInfo(object): #{{{
22     def __init__(self, user):
23         try:
24             info = pwd.getpwnam(user)
25         except:
26             info = pwd.get(int(user))
27         self.user = info[0]
28         self.uid = info[2]
29         self.gid = info[3]
30         self.name = info[4]
31         self.home = info[5]
32         self.shell = info[6]
33         self.group = grp.getgrgid(self.gid)[0]
34 #}}}
35
36 user_info = UserInfo(config.get('sercom.tester.user', 65534))
37
38 def check_call(*popenargs, **kwargs): #{{{ XXX Python 2.5 forward-compatibility
39     """Run command with arguments.  Wait for command to complete.  If
40     the exit code was zero then return, otherwise raise
41     CalledProcessError.  The CalledProcessError object will have the
42     return code in the returncode attribute.
43     ret = call(*popenargs, **kwargs)
44
45     The arguments are the same as for the Popen constructor.  Example:
46
47     check_call(["ls", "-l"])
48     """
49     retcode = sp.call(*popenargs, **kwargs)
50     cmd = kwargs.get("args")
51     if cmd is None:
52         cmd = popenargs[0]
53     if retcode:
54         raise sp.CalledProcessError(retcode, cmd)
55     return retcode
56 sp.check_call = check_call
57 #}}}
58
59 #{{{ Excepciones
60
61 class CalledProcessError(Exception): #{{{ XXX Python 2.5 forward-compatibility
62     """This exception is raised when a process run by check_call() returns
63     a non-zero exit status.  The exit status will be stored in the
64     returncode attribute."""
65     def __init__(self, returncode, cmd):
66         self.returncode = returncode
67         self.cmd = cmd
68     def __str__(self):
69         return ("Command '%s' returned non-zero exit status %d"
70             % (self.cmd, self.returncode))
71 sp.CalledProcessError = CalledProcessError
72 #}}}
73
74 class Error(StandardError): pass
75
76 class ExecutionFailure(Error, RuntimeError): #{{{
77     def __init__(self, comando, tarea=None, caso_de_prueba=None):
78         self.comando = comando
79         self.tarea = tarea
80         self.caso_de_prueba = caso_de_prueba
81 #}}}
82
83 #}}}
84
85 def unzip(bytes, default_dst='.', specific_dst=dict()): # {{{
86     u"""Descomprime un buffer de datos en formato ZIP.
87     Los archivos se descomprimen en default_dst a menos que exista una entrada
88     en specific_dst cuya clave sea el nombre de archivo a descomprimir, en
89     cuyo caso, se descomprime usando como destino el valor de dicha clave.
90     """
91     log.debug(_(u'Intentando descomprimir'))
92     if bytes is None:
93         return
94     zfile = ZipFile(StringIO(bytes), 'r')
95     for f in zfile.namelist():
96         dst = join(specific_dst.get(f, default_dst), f)
97         if f.endswith(os.sep):
98             log.debug(_(u'Creando directorio "%s" en "%s"'), f, dst)
99             os.mkdir(dst)
100         else:
101             log.debug(_(u'Descomprimiendo archivo "%s" en "%s"'), f, dst)
102             file(dst, 'w').write(zfile.read(f))
103     zfile.close()
104 #}}}
105
106 class Multizip(object): #{{{
107     def __init__(self, *zip_streams):
108         self.zips = [ZipFile(StringIO(z), 'r') for z in zip_streams
109             if z is not None]
110         self.names = set()
111         for z in self.zips:
112             self.names |= set(z.namelist())
113     def read(self, name):
114         for z in self.zips:
115             try:
116                 return z.read(name)
117             except KeyError:
118                 pass
119         raise KeyError(name)
120     def namelist(self):
121         return self.names
122 #}}}
123
124 class SecureProcess(object): #{{{
125     default = dict(
126         max_tiempo_cpu      = 120,
127         max_memoria         = 16,
128         max_tam_archivo     = 5,
129         max_cant_archivos   = 5,
130         max_cant_procesos   = 0,
131         max_locks_memoria   = 0,
132     )
133     uid = config.get('sercom.tester.chroot.user', 65534)
134     MB = 1048576
135     # XXX probar! make de un solo archivo lleva nproc=100 y nofile=15
136     def __init__(self, comando, chroot, cwd, close_stdin=False,
137                  close_stdout=False, close_stderr=False):
138         self.comando = comando
139         self.chroot = chroot
140         self.cwd = cwd
141         self.close_stdin = close_stdin
142         self.close_stdout = close_stdout
143         self.close_stderr = close_stderr
144         log.debug(_(u'Proceso segurizado: chroot=%s, cwd=%s, user=%s, cpu=%s, '
145             u'as=%sMiB, fsize=%sMiB, nofile=%s, nproc=%s, memlock=%s'),
146             self.chroot, self.cwd, self.uid, self.max_tiempo_cpu,
147             self.max_memoria, self.max_tam_archivo, self.max_cant_archivos,
148             self.max_cant_procesos, self.max_locks_memoria)
149     def __getattr__(self, name):
150         if getattr(self.comando, name) is not None:
151             return getattr(self.comando, name)
152         return config.get('sercom.tester.limits.' + name, self.default[name])
153     def __call__(self):
154         x2 = lambda x: (x, x)
155         if self.close_stdin:
156             os.close(0)
157         if self.close_stdout:
158             os.close(1)
159         if self.close_stderr:
160             os.close(2)
161         os.chroot(self.chroot)
162         os.chdir(self.cwd)
163         uinfo = UserInfo(self.uid)
164         os.setgid(uinfo.gid)
165         os.setuid(uinfo.uid) # Somos mortales irreversiblemente
166         rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
167         rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
168         rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
169         rsrc.setrlimit(rsrc.RLIMIT_NOFILE, x2(self.max_cant_archivos)) #XXX Obtener de archivos esperados?
170         rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
171         rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
172         rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
173         # Tratamos de forzar un sync para que entre al sleep del padre FIXME
174         import time
175         time.sleep(0)
176 #}}}
177
178 class Tester(object): #{{{
179
180     def __init__(self, name, path, home, queue): #{{{ y properties
181         self.name = name
182         self.path = path
183         self.home = home
184         self.queue = queue
185         # Ahora somos mortales (oid mortales)
186         os.setegid(user_info.gid)
187         os.seteuid(user_info.uid)
188         log.debug(_(u'usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
189             user_info.user, user_info.group, user_info.uid, user_info.gid)
190
191     @property
192     def build_path(self):
193         return join(self.chroot, self.home, 'build')
194
195     @property
196     def test_path(self):
197         return join(self.chroot, self.home, 'test')
198
199     @property
200     def chroot(self):
201         return join(self.path, 'chroot_' + self.name)
202
203     @property
204     def orig_chroot(self):
205         return join(self.path, 'chroot')
206     #}}}
207
208     def run(self): #{{{
209         entrega_id = self.queue.get() # blocking
210         while entrega_id is not None:
211             entrega = Entrega.get(entrega_id)
212             log.debug(_(u'Nueva entrega para probar en tester %s: %s'),
213                 self.name, entrega)
214             self.test(entrega)
215             log.debug(_(u'Fin de pruebas de: %s'), entrega)
216             entrega_id = self.queue.get() # blocking
217     #}}}
218
219     def test(self, entrega): #{{{
220         log.debug(_(u'Tester.test(entrega=%s)'), entrega)
221         entrega.inicio = datetime.now()
222         try:
223             try:
224                 self.setup_chroot(entrega)
225                 self.ejecutar_tareas_fuente(entrega)
226                 self.ejecutar_tareas_prueba(entrega)
227                 self.clean_chroot(entrega)
228             except ExecutionFailure, e:
229                 pass
230             except Exception, e:
231                 if isinstance(e, SystemExit): raise
232                 entrega.observaciones += error_interno
233                 log.exception(_('Hubo una excepcion inesperada')) # FIXME encoding
234             except:
235                 entrega.observaciones += error_interno
236                 log.exception(_('Hubo una excepcion inesperada desconocida')) # FIXME encoding
237         finally:
238             entrega.fin = datetime.now()
239             if entrega.exito is None:
240                 entrega.exito = True
241             if entrega.exito:
242                 log.info(_(u'Entrega correcta: %s'), entrega)
243             else:
244                 log.info(_(u'Entrega incorrecta: %s'), entrega)
245     #}}}
246
247     def setup_chroot(self, entrega): #{{{ y clean_chroot()
248         log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
249         rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
250             '--archive', '--acls', '--delete-during', '--force',
251             '--exclude', '/proc', # TODO config
252             join(self.orig_chroot, ''), self.chroot)
253         log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
254         os.seteuid(0) # Dios! (para chroot)
255         os.setegid(0)
256         try:
257             sp.check_call(rsync)
258         finally:
259             os.setegid(user_info.gid) # Mortal de nuevo
260             os.seteuid(user_info.uid)
261             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
262                 user_info.user, user_info.group, user_info.uid, user_info.gid)
263         unzip(entrega.archivos, self.build_path)
264
265     def clean_chroot(self, entrega):
266         log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
267         pass # Se limpia con el próximo rsync
268     #}}}
269
270     def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
271         log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
272             entrega.shortrepr())
273         tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
274                     if isinstance(t, TareaFuente)]
275         for tarea in tareas:
276             tarea.ejecutar(self.build_path, entrega)
277
278     def ejecutar_tareas_prueba(self, entrega):
279         log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
280             entrega.shortrepr())
281         for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
282             caso.ejecutar(self.test_path, entrega)
283     #}}}
284
285 #}}}
286
287 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
288     log.debug(_(u'CasoDePrueba.ejecutar(caso=%s, path=%s, entrega=%s)'), self,
289         path, entrega.shortrepr())
290     tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
291                 if isinstance(t, TareaPrueba)]
292     prueba = entrega.add_prueba(self, inicio=datetime.now())
293     try:
294         try:
295             for tarea in tareas:
296                 tarea.ejecutar(path, prueba)
297         except ExecutionFailure, e:
298             pass
299     finally:
300         prueba.fin = datetime.now()
301         if prueba.exito is None:
302             prueba.exito = True
303     if not prueba.exito and self.rechazar_si_falla:
304         entrega.exito = False
305     if not prueba.exito and self.terminar_si_falla:
306         raise ExecutionFailure(prueba)
307 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
308 #}}}
309
310 def ejecutar_tarea(self, path, ejecucion): #{{{
311     log.debug(_(u'Tarea.ejecutar(path=%s, ejecucion=%s)'), path,
312         ejecucion.shortrepr())
313     for cmd in self.comandos:
314         cmd.ejecutar(path, ejecucion)
315 Tarea.ejecutar = ejecutar_tarea
316 #}}}
317
318 # TODO generalizar ejecutar_comando_xxxx!!!
319
320 def ejecutar_comando_fuente(self, path, entrega): #{{{
321     log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
322         entrega.shortrepr())
323     comando_ejecutado = entrega.add_comando_ejecutado(self)
324     basetmp = '/tmp/sercom.tester.fuente' # FIXME TODO /var/run/sercom?
325     unzip(self.archivos_entrada, path, # TODO try/except
326         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
327     options = dict(
328         close_fds=True,
329         shell=True,
330         preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build') #FIXME!!! path
331     )
332     if os.path.exists('%s.%s.stdin' % (basetmp, comando_ejecutado.id)):
333         options['stdin'] = file('%s.%s.stdin' % (basetmp, comando_ejecutado.id),
334             'r')
335     else:
336         options['preexec_fn'].close_stdin = True
337     a_guardar = set(self.archivos_a_guardar)
338     zip_a_comparar = Multizip(self.archivos_a_comparar)
339     a_comparar = set(zip_a_comparar.namelist())
340     a_usar = frozenset(a_guardar | a_comparar)
341     if self.STDOUTERR in a_usar:
342         options['stdout'] = file('%s.%s.stdouterr' % (basetmp,
343             comando_ejecutado.id), 'w')
344         options['stderr'] = sp.STDOUT
345     else:
346         if self.STDOUT in a_usar:
347             options['stdout'] = file('%s.%s.stdout' % (basetmp,
348                 comando_ejecutado.id), 'w')
349         else:
350             options['preexec_fn'].close_stdout = True
351         if self.STDERR in a_usar:
352             options['stderr'] = file('%s.%s.stderr' % (basetmp,
353                 comando_ejecutado.id), 'w')
354         else:
355             options['preexec_fn'].close_stderr = True
356     comando = self.comando # FIXME Acá tiene que diferenciarse de ComandoPrueba
357     comando_ejecutado.inicio = datetime.now()
358     log.debug(_(u'Ejecutando como root: %s'), comando)
359     os.seteuid(0) # Dios! (para chroot)
360     os.setegid(0)
361     try:
362         try:
363             proc = sp.Popen(comando, **options)
364         finally:
365             os.setegid(user_info.gid) # Mortal de nuevo
366             os.seteuid(user_info.uid)
367             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
368                 user_info.user, user_info.group, user_info.uid, user_info.gid)
369     except Exception, e:
370         if hasattr(e, 'child_traceback'):
371             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
372         raise
373     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
374     comando_ejecutado.fin = datetime.now()
375     retorno = self.retorno
376     if retorno != self.RET_ANY:
377         if retorno == self.RET_FAIL:
378             if proc.returncode == 0:
379                 if self.rechazar_si_falla:
380                     entrega.exito = False
381                 comando_ejecutado.exito = False
382                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
383                     u'programa termine con un error (código de retorno '
384                     u'distinto de 0) pero terminó bien (código de retorno '
385                     u'0).\n')
386                 log.debug(_(u'Se esperaba que el programa termine '
387                     u'con un error (código de retorno distinto de 0) pero '
388                     u'terminó bien (código de retorno 0).\n'))
389         elif retorno != proc.returncode:
390             if self.rechazar_si_falla:
391                 entrega.exito = False
392             comando_ejecutado.exito = False
393             if proc.returncode < 0:
394                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
395                     u'con un código de retorno %s pero se obtuvo una señal %s '
396                     u'(%s).\n') % (retorno, -proc.returncode, -proc.returncode) # TODO poner con texto
397                 log.debug(_(u'Se esperaba terminar con un código '
398                     u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
399                     retorno, -proc.returncode, -proc.returncode)
400             else:
401                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
402                     u'con un código de retorno %s pero se obtuvo %s.\n') \
403                     % (retorno, proc.returncode)
404                 log.debug(_(u'Se esperaba terminar con un código de retorno '
405                     u'%s pero se obtuvo %s.\n'), retorno, proc.returncode)
406     if comando_ejecutado.exito is None:
407         log.debug(_(u'Código de retorno OK'))
408     if a_guardar:
409         buffer = StringIO()
410         zip = ZipFile(buffer, 'w')
411         # Guardamos stdout/stderr
412         if self.STDOUTERR in a_guardar:
413             a_guardar.remove(self.STDOUTERR)
414             zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
415                 self.STDOUTERR)
416         else:
417             if self.STDOUT in a_guardar:
418                 a_guardar.remove(self.STDOUT)
419                 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
420                     self.STDOUT)
421             if self.STDERR in a_guardar:
422                 a_guardar.remove(self.STDERR)
423                 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
424                     self.STDERR)
425         # Guardamos otros
426         for f in a_guardar:
427             if not os.path.exists(join(path, f)):
428                 if self.rechazar_si_falla:
429                     entrega.exito = False
430                 comando_ejecutado.exito = False
431                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
432                     u'"%s" para guardar pero no fue encontrado.\n') % f
433                 log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
434                     u'no fue encontrado'), f)
435             else:
436                 zip.write(join(path, f), f)
437         zip.close()
438         comando_ejecutado.archivos = buffer.getvalue()
439     def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
440              newname='entregado'):
441         if longname is None:
442             longname = name
443         new = file(new, 'r').readlines()
444         orig = zip_in.read(name).splitlines(True)
445         udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
446             tofile=name+'.'+newname)))
447         if udiff:
448             if self.rechazar_si_falla:
449                 entrega.exito = False
450             comando_ejecutado.exito = False
451             comando_ejecutado.observaciones += _(u'%s no coincide con lo '
452                 u'esperado (archivo "%s.diff").\n') % (longname, name)
453             log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
454                 longname, name)
455             htmldiff = HtmlDiff().make_file(orig, new,
456                 fromdesc=name+'.'+origname, todesc=name+'.'+newname,
457                 context=True, numlines=3)
458             zip_out.writestr(name + '.diff', udiff)
459             zip_out.writestr(name + '.diff', htmldiff)
460             return True
461         else:
462             return False
463     if a_comparar:
464         condiff = False
465         buffer = StringIO()
466         zip = ZipFile(buffer, 'w')
467         # Comparamos stdout/stderr
468         if self.STDOUTERR in a_comparar:
469             a_comparar.remove(self.STDOUTERR)
470             condiff |= diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
471                 zip_a_comparar, zip, self.STDOUTERR,
472                 _(u'La salida estándar y de error combinada'))
473         else:
474             if self.STDOUT in a_comparar:
475                 a_comparar.remove(self.STDOUT)
476                 condiff |= diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
477                     zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
478             if self.STDERR in a_comparar:
479                 a_comparar.remove(self.STDERR)
480                 condiff |= diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
481                     zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
482         # Comparamos otros
483         for f in a_comparar:
484             if not os.path.exists(join(path, f)):
485                 if self.rechazar_si_falla:
486                     entrega.exito = False
487                 comando_ejecutado.exito = False
488                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
489                     u'"%s" para comparar pero no fue encontrado') % f
490                 log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
491                     u'no fue encontrado'), f)
492             else:
493                 condiff |= diff(join(path, f), zip_a_comparar, zip, f)
494         zip.close()
495         if condiff:
496             comando_ejecutado.diferencias = buffer.getvalue()
497     if comando_ejecutado.exito is None:
498         comando_ejecutado.exito = True
499     elif self.terminar_si_falla:
500         raise ExecutionFailure(self)
501
502 ComandoFuente.ejecutar = ejecutar_comando_fuente
503 #}}}
504
505 def ejecutar_comando_prueba(self, path, prueba): #{{{
506     # Diferencia con comando fuente: s/entrega/prueba/ y s/build/test/ en path
507     # y setup/clean de test.
508     log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
509         prueba.shortrepr())
510     caso_de_prueba = prueba.caso_de_prueba
511     comando_ejecutado = prueba.add_comando_ejecutado(self)
512     basetmp = '/tmp/sercom.tester.prueba' # FIXME TODO /var/run/sercom?
513     #{{{ Código que solo va en ComandoPrueba (setup de directorio)
514     rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
515         '--archive', '--acls', '--delete-during', '--force', # TODO config
516         'var/chroot_pepe/home/sercom/build/', path) # FIXME!!!! path
517     log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
518     os.seteuid(0) # Dios! (para chroot)
519     os.setegid(0)
520     try:
521         sp.check_call(rsync)
522     finally:
523         os.setegid(user_info.gid) # Mortal de nuevo
524         os.seteuid(user_info.uid)
525         log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
526             user_info.user, user_info.group, user_info.uid, user_info.gid)
527     #}}}
528     unzip(self.archivos_entrada, path, # TODO try/except
529         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
530     unzip(caso_de_prueba.archivos_entrada, path, # TODO try/except     # FIXME Esto es propio de ComandoPrueba
531         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)}) # FIXME Esto es propio de ComandoPrueba
532     options = dict(
533         close_fds=True,
534         shell=True,
535         preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/test') # FIXME!!!! path
536     )
537     if os.path.exists('%s.%s.stdin' % (basetmp, comando_ejecutado.id)):
538         options['stdin'] = file('%s.%s.stdin' % (basetmp, comando_ejecutado.id),
539             'r')
540     else:
541         options['preexec_fn'].close_stdin = True
542     a_guardar = set(self.archivos_a_guardar)
543     a_guardar |= set(caso_de_prueba.archivos_a_guardar)           # FIXME Esto es propio de ComandoPrueba
544     log.debug('archivos a guardar: %s', a_guardar)
545     zip_a_comparar = Multizip(caso_de_prueba.archivos_a_comparar, # FIXME Esto es propio de ComandoPrueba
546         self.archivos_a_comparar)                                 # FIXME Esto es propio de ComandoPrueba
547     a_comparar = set(zip_a_comparar.namelist())
548     log.debug('archivos a comparar: %s', a_comparar)
549     a_usar = frozenset(a_guardar | a_comparar)
550     log.debug('archivos a usar: %s', a_usar)
551     if self.STDOUTERR in a_usar:
552         options['stdout'] = file('%s.%s.stdouterr' % (basetmp,
553             comando_ejecutado.id), 'w')
554         options['stderr'] = sp.STDOUT
555     else:
556         if self.STDOUT in a_usar:
557             log.debug('capurando salida en: %s.%s.stdout', basetmp, comando_ejecutado.id)
558             options['stdout'] = file('%s.%s.stdout' % (basetmp,
559                 comando_ejecutado.id), 'w')
560         else:
561             options['preexec_fn'].close_stdout = True
562         if self.STDERR in a_usar:
563             options['stderr'] = file('%s.%s.stderr' % (basetmp,
564                 comando_ejecutado.id), 'w')
565         else:
566             options['preexec_fn'].close_stderr = True
567     comando = self.comando + ' ' + caso_de_prueba.comando # FIXME Esto es propio de ComandoPrueba
568     comando_ejecutado.inicio = datetime.now()
569     log.debug(_(u'Ejecutando como root: %s'), comando)
570     os.seteuid(0) # Dios! (para chroot)
571     os.setegid(0)
572     try:
573         try:
574             proc = sp.Popen(comando, **options)
575         finally:
576             os.setegid(user_info.gid) # Mortal de nuevo
577             os.seteuid(user_info.uid)
578             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
579                 user_info.user, user_info.group, user_info.uid, user_info.gid)
580     except Exception, e:
581         if hasattr(e, 'child_traceback'):
582             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
583         raise
584     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
585     comando_ejecutado.fin = datetime.now()
586     retorno = self.retorno
587     if retorno == self.RET_PRUEBA:                # FIXME Esto es propio de ComandoPrueba
588         retorno = caso_de_prueba.retorno   # FIXME Esto es propio de ComandoPrueba
589     if retorno != self.RET_ANY:
590         if retorno == self.RET_FAIL:
591             if proc.returncode == 0:
592                 if self.rechazar_si_falla:
593                     prueba.exito = False
594                 comando_ejecutado.exito = False
595                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
596                     u'programa termine con un error (código de retorno '
597                     u'distinto de 0) pero terminó bien (código de retorno '
598                     u'0).\n')
599                 log.debug(_(u'Se esperaba que el programa termine '
600                     u'con un error (código de retorno distinto de 0) pero '
601                     u'terminó bien (código de retorno 0).\n'))
602         elif retorno != proc.returncode:
603             if self.rechazar_si_falla:
604                 prueba.exito = False
605             comando_ejecutado.exito = False
606             if proc.returncode < 0:
607                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
608                     u'con un código de retorno %s pero se obtuvo una señal %s '
609                     u'(%s).\n') % (retorno, -proc.returncode, -proc.returncode) # TODO poner con texto
610                 log.debug(_(u'Se esperaba terminar con un código '
611                     u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
612                     retorno, -proc.returncode, -proc.returncode)
613             else:
614                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
615                     u'con un código de retorno %s pero se obtuvo %s.\n') \
616                     % (retorno, proc.returncode)
617                 log.debug(_(u'Se esperaba terminar con un código de retorno '
618                     u'%s pero se obtuvo %s.\n'), retorno, proc.returncode)
619     if comando_ejecutado.exito is None:
620         log.debug(_(u'Código de retorno OK'))
621     if a_guardar:
622         buffer = StringIO()
623         zip = ZipFile(buffer, 'w')
624         # Guardamos stdout/stderr
625         if self.STDOUTERR in a_guardar:
626             a_guardar.remove(self.STDOUTERR)
627             zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
628                 self.STDOUTERR)
629         else:
630             if self.STDOUT in a_guardar:
631                 a_guardar.remove(self.STDOUT)
632                 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
633                     self.STDOUT)
634             if self.STDERR in a_guardar:
635                 a_guardar.remove(self.STDERR)
636                 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
637                     self.STDERR)
638         # Guardamos otros
639         for f in a_guardar:
640             if not os.path.exists(join(path, f)):
641                 if self.rechazar_si_falla:
642                     prueba.exito = False
643                 comando_ejecutado.exito = False
644                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
645                     u'"%s" para guardar pero no fue encontrado.\n') % f
646                 log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
647                     u'no fue encontrado'), f)
648             else:
649                 zip.write(join(path, f), f)
650         zip.close()
651         comando_ejecutado.archivos = buffer.getvalue()
652     def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
653              newname='entregado'):
654         if longname is None:
655             longname = name
656         new = file(new, 'r').readlines()
657         orig = zip_in.read(name).splitlines(True)
658         udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
659             tofile=name+'.'+newname)))
660         if udiff:
661             if self.rechazar_si_falla:
662                 prueba.exito = False
663             comando_ejecutado.exito = False
664             comando_ejecutado.observaciones += _(u'%s no coincide con lo '
665                 u'esperado (archivo "%s.diff").\n') % (longname, name)
666             log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
667                 longname, name)
668             htmldiff = HtmlDiff().make_file(orig, new,
669                 fromdesc=name+'.'+origname, todesc=name+'.'+newname,
670                 context=True, numlines=3)
671             zip_out.writestr(name + '.diff', udiff)
672             zip_out.writestr(name + '.html', htmldiff)
673             return True
674         else:
675             return False
676     if a_comparar:
677         condiff = False
678         buffer = StringIO()
679         zip = ZipFile(buffer, 'w')
680         # Comparamos stdout/stderr
681         if self.STDOUTERR in a_comparar:
682             a_comparar.remove(self.STDOUTERR)
683             condiff |= diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
684                 zip_a_comparar, zip, self.STDOUTERR,
685                 _(u'La salida estándar y de error combinada'))
686         else:
687             if self.STDOUT in a_comparar:
688                 log.debug('comparando salida con: %s.%s.stdout', basetmp, comando_ejecutado.id)
689                 a_comparar.remove(self.STDOUT)
690                 condiff |= diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
691                     zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
692             if self.STDERR in a_comparar:
693                 a_comparar.remove(self.STDERR)
694                 condiff |= diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
695                     zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
696         # Comparamos otros
697         for f in a_comparar:
698             if not os.path.exists(join(path, f)):
699                 if self.rechazar_si_falla:
700                     prueba.exito = False
701                 comando_ejecutado.exito = False
702                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
703                     u'"%s" para comparar pero no fue encontrado') % f
704                 log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
705                     u'no fue encontrado'), f)
706             else:
707                 condiff |= diff(join(path, f), zip_a_comparar, zip, f)
708         zip.close()
709         if condiff:
710             comando_ejecutado.diferencias = buffer.getvalue()
711     if comando_ejecutado.exito is None:
712         comando_ejecutado.exito = True
713     elif self.terminar_si_falla:
714         raise ExecutionFailure(self)
715
716 ComandoPrueba.ejecutar = ejecutar_comando_prueba
717 #}}}
718