]> git.llucax.com Git - software/sercom.git/blob - sercom/tester.py
Priorizar archivos del caso de prueba sobre los del comando.
[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', # TODO config
251             join(self.orig_chroot, ''), self.chroot)
252         log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
253         os.seteuid(0) # Dios! (para chroot)
254         os.setegid(0)
255         try:
256             sp.check_call(rsync)
257         finally:
258             os.setegid(user_info.gid) # Mortal de nuevo
259             os.seteuid(user_info.uid)
260             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
261                 user_info.user, user_info.group, user_info.uid, user_info.gid)
262         unzip(entrega.archivos, self.build_path)
263
264     def clean_chroot(self, entrega):
265         log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
266         pass # Se limpia con el próximo rsync
267     #}}}
268
269     def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
270         log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
271             entrega.shortrepr())
272         tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
273                     if isinstance(t, TareaFuente)]
274         for tarea in tareas:
275             tarea.ejecutar(self.build_path, entrega)
276
277     def ejecutar_tareas_prueba(self, entrega):
278         log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
279             entrega.shortrepr())
280         for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
281             caso.ejecutar(self.test_path, entrega)
282     #}}}
283
284 #}}}
285
286 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
287     log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
288         entrega.shortrepr())
289     tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
290                 if isinstance(t, TareaPrueba)]
291     prueba = entrega.add_prueba(self)
292     try:
293         try:
294             for tarea in tareas:
295                 tarea.ejecutar(path, prueba)
296         except ExecutionFailure, e:
297             pass
298     finally:
299         prueba.fin = datetime.now()
300         if prueba.exito is None:
301             prueba.exito = True
302     if not prueba.exito and self.rechazar_si_falla:
303         entrega.exito = False
304     if not prueba.exito and self.terminar_si_falla:
305         raise ExecutionFailure(prueba)
306 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
307 #}}}
308
309 def ejecutar_tarea(self, path, ejecucion): #{{{
310     log.debug(_(u'Tarea.ejecutar(path=%s, ejecucion=%s)'), path,
311         ejecucion.shortrepr())
312     for cmd in self.comandos:
313         cmd.ejecutar(path, ejecucion)
314 Tarea.ejecutar = ejecutar_tarea
315 #}}}
316
317 # TODO generalizar ejecutar_comando_xxxx!!!
318
319 def ejecutar_comando_fuente(self, path, entrega): #{{{
320     log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
321         entrega.shortrepr())
322     comando_ejecutado = entrega.add_comando_ejecutado(self) # TODO debería rodear solo la ejecución del comando
323     basetmp = '/tmp/sercom.tester.fuente' # FIXME TODO /var/run/sercom?
324     unzip(self.archivos_entrada, path, # TODO try/except
325         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
326     options = dict(
327         close_fds=True,
328         shell=True,
329         preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build') #FIXME!!! path
330     )
331     if os.path.exists('%s.%s.stdin' % (basetmp, comando_ejecutado.id)):
332         options['stdin'] = file('%s.%s.stdin' % (basetmp, comando_ejecutado.id),
333             'r')
334     else:
335         options['preexec_fn'].close_stdin = True
336     a_guardar = set(self.archivos_a_guardar)
337     zip_a_comparar = Multizip(self.archivos_a_comparar)
338     a_comparar = set(zip_a_comparar.namelist())
339     a_usar = frozenset(a_guardar | a_comparar)
340     if self.STDOUTERR in a_usar:
341         options['stdout'] = file('%s.%s.stdouterr' % (basetmp,
342             comando_ejecutado.id), 'w')
343         options['stderr'] = sp.STDOUT
344     else:
345         if self.STDOUT in a_usar:
346             options['stdout'] = file('%s.%s.stdout' % (basetmp,
347                 comando_ejecutado.id), 'w')
348         else:
349             options['preexec_fn'].close_stdout = True
350         if self.STDERR in a_usar:
351             options['stderr'] = file('%s.%s.stderr' % (basetmp,
352                 comando_ejecutado.id), 'w')
353         else:
354             options['preexec_fn'].close_stderr = True
355     comando = self.comando # FIXME Acá tiene que diferenciarse de ComandoPrueba
356     log.debug(_(u'Ejecutando como root: %s'), comando)
357     os.seteuid(0) # Dios! (para chroot)
358     os.setegid(0)
359     try:
360         try:
361             proc = sp.Popen(comando, **options)
362         finally:
363             os.setegid(user_info.gid) # Mortal de nuevo
364             os.seteuid(user_info.uid)
365             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
366                 user_info.user, user_info.group, user_info.uid, user_info.gid)
367     except Exception, e:
368         if hasattr(e, 'child_traceback'):
369             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
370         raise
371     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
372     comando_ejecutado.fin = datetime.now() # TODO debería rodear solo la ejecución del comando
373     retorno = self.retorno
374     if retorno != self.RET_ANY:
375         if retorno == self.RET_FAIL:
376             if proc.returncode == 0:
377                 if self.rechazar_si_falla:
378                     entrega.exito = False
379                 comando_ejecutado.exito = False
380                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
381                     u'programa termine con un error (código de retorno '
382                     u'distinto de 0) pero terminó bien (código de retorno '
383                     u'0).\n')
384                 log.debug(_(u'Se esperaba que el programa termine '
385                     u'con un error (código de retorno distinto de 0) pero '
386                     u'terminó bien (código de retorno 0).\n'))
387         elif retorno != proc.returncode:
388             if self.rechazar_si_falla:
389                 entrega.exito = False
390             comando_ejecutado.exito = False
391             if proc.returncode < 0:
392                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
393                     u'con un código de retorno %s pero se obtuvo una señal %s '
394                     u'(%s).\n') % (retorno, -proc.returncode, -proc.returncode) # TODO poner con texto
395                 log.debug(_(u'Se esperaba terminar con un código '
396                     u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
397                     retorno, -proc.returncode, -proc.returncode)
398             else:
399                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
400                     u'con un código de retorno %s pero se obtuvo %s.\n') \
401                     % (retorno, proc.returncode)
402                 log.debug(_(u'Se esperaba terminar con un código de retorno '
403                     u'%s pero se obtuvo %s.\n'), retorno, proc.returncode)
404     if comando_ejecutado.exito is None:
405         log.debug(_(u'Código de retorno OK'))
406     if a_guardar:
407         buffer = StringIO()
408         zip = ZipFile(buffer, 'w')
409         # Guardamos stdout/stderr
410         if self.STDOUTERR in a_guardar:
411             a_guardar.remove(self.STDOUTERR)
412             zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
413                 self.STDOUTERR)
414         else:
415             if self.STDOUT in a_guardar:
416                 a_guardar.remove(self.STDOUT)
417                 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
418                     self.STDOUT)
419             if self.STDERR in a_guardar:
420                 a_guardar.remove(self.STDERR)
421                 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
422                     self.STDERR)
423         # Guardamos otros
424         for f in a_guardar:
425             if not os.path.exists(join(path, f)):
426                 if self.rechazar_si_falla:
427                     entrega.exito = False
428                 comando_ejecutado.exito = False
429                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
430                     u'"%s" para guardar pero no fue encontrado.\n') % f
431                 log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
432                     u'no fue encontrado'), f)
433             else:
434                 zip.write(join(path, f), f)
435         zip.close()
436         comando_ejecutado.archivos = buffer.getvalue()
437     def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
438              newname='entregado'):
439         if longname is None:
440             longname = name
441         new = file(new, 'r').readlines()
442         orig = zip_in.read(name).split('\n')
443         udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
444             tofile=name+'.'+newname)))
445         if udiff:
446             if self.rechazar_si_falla:
447                 entrega.exito = False
448             comando_ejecutado.exito = False
449             comando_ejecutado.observaciones += _(u'%s no coincide con lo '
450                 u'esperado (archivo "%s.diff").\n') % (longname, name)
451             log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
452                 longname, name)
453             htmldiff = HtmlDiff().make_file(orig, new,
454                 fromdesc=name+'.'+origname, todesc=name+'.'+newname,
455                 context=True, numlines=3)
456             zip_out.writestr(name + '.diff', udiff)
457             zip_out.writestr(name + '.diff.html', htmldiff)
458             return True
459         else:
460             return False
461     if a_comparar:
462         buffer = StringIO()
463         zip = ZipFile(buffer, 'w')
464         # Comparamos stdout/stderr
465         if self.STDOUTERR in a_comparar:
466             a_comparar.remove(self.STDOUTERR)
467             diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
468                 zip_a_comparar, zip, self.STDOUTERR,
469                 _(u'La salida estándar y de error combinada'))
470         else:
471             if self.STDOUT in a_comparar:
472                 a_comparar.remove(self.STDOUT)
473                 diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
474                     zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
475             if self.STDERR in a_comparar:
476                 a_comparar.remove(self.STDERR)
477                 diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
478                     zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
479         # Comparamos otros
480         for f in a_comparar:
481             if not os.path.exists(join(path, f)):
482                 if self.rechazar_si_falla:
483                     entrega.exito = False
484                 comando_ejecutado.exito = False
485                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
486                     u'"%s" para comparar pero no fue encontrado') % f
487                 log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
488                     u'no fue encontrado'), f)
489             else:
490                 diff(join(path, f), zip_a_comparar, zip, f)
491         zip.close()
492         comando_ejecutado.diferencias = buffer.getvalue()
493     if comando_ejecutado.exito is None:
494         comando_ejecutado.exito = True
495     elif self.terminar_si_falla:
496         raise ExecutionFailure(self)
497
498 ComandoFuente.ejecutar = ejecutar_comando_fuente
499 #}}}
500
501 def ejecutar_comando_prueba(self, path, prueba): #{{{
502     # Diferencia con comando fuente: s/entrega/prueba/ y s/build/test/ en path
503     # y setup/clean de test.
504     log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
505         prueba.shortrepr())
506     caso_de_prueba = prueba.caso_de_prueba
507     comando_ejecutado = prueba.add_comando_ejecutado(self) # TODO debería rodear solo la ejecución del comando
508     basetmp = '/tmp/sercom.tester.prueba' # FIXME TODO /var/run/sercom?
509     #{{{ Código que solo va en ComandoPrueba (setup de directorio)
510     rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
511         '--archive', '--acls', '--delete-during', '--force', # TODO config
512         'var/chroot_pepe/home/sercom/build/', path) # FIXME!!!! path
513     log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
514     os.seteuid(0) # Dios! (para chroot)
515     os.setegid(0)
516     try:
517         sp.check_call(rsync)
518     finally:
519         os.setegid(user_info.gid) # Mortal de nuevo
520         os.seteuid(user_info.uid)
521         log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
522             user_info.user, user_info.group, user_info.uid, user_info.gid)
523     #}}}
524     unzip(self.archivos_entrada, path, # TODO try/except
525         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
526     unzip(caso_de_prueba.archivos_entrada, path, # TODO try/except     # FIXME Esto es propio de ComandoPrueba
527         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)}) # FIXME Esto es propio de ComandoPrueba
528     options = dict(
529         close_fds=True,
530         shell=True,
531         preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/test') # FIXME!!!! path
532     )
533     if os.path.exists('%s.%s.stdin' % (basetmp, comando_ejecutado.id)):
534         options['stdin'] = file('%s.%s.stdin' % (basetmp, comando_ejecutado.id),
535             'r')
536     else:
537         options['preexec_fn'].close_stdin = True
538     a_guardar = set(self.archivos_a_guardar)
539     a_guardar |= set(caso_de_prueba.archivos_a_guardar)           # FIXME Esto es propio de ComandoPrueba
540     zip_a_comparar = Multizip(caso_de_prueba.archivos_a_comparar, # FIXME Esto es propio de ComandoPrueba
541         self.archivos_a_comparar)                                 # FIXME Esto es propio de ComandoPrueba
542     a_comparar = set(zip_a_comparar.namelist())
543     a_usar = frozenset(a_guardar | a_comparar)
544     if self.STDOUTERR in a_usar:
545         options['stdout'] = file('%s.%s.stdouterr' % (basetmp,
546             comando_ejecutado.id), 'w')
547         options['stderr'] = sp.STDOUT
548     else:
549         if self.STDOUT in a_usar:
550             options['stdout'] = file('%s.%s.stdout' % (basetmp,
551                 comando_ejecutado.id), 'w')
552         else:
553             options['preexec_fn'].close_stdout = True
554         if self.STDERR in a_usar:
555             options['stderr'] = file('%s.%s.stderr' % (basetmp,
556                 comando_ejecutado.id), 'w')
557         else:
558             options['preexec_fn'].close_stderr = True
559     comando = self.comando + ' ' + caso_de_prueba.comando # FIXME Esto es propio de ComandoPrueba
560     log.debug(_(u'Ejecutando como root: %s'), comando)
561     os.seteuid(0) # Dios! (para chroot)
562     os.setegid(0)
563     try:
564         try:
565             proc = sp.Popen(comando, **options)
566         finally:
567             os.setegid(user_info.gid) # Mortal de nuevo
568             os.seteuid(user_info.uid)
569             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
570                 user_info.user, user_info.group, user_info.uid, user_info.gid)
571     except Exception, e:
572         if hasattr(e, 'child_traceback'):
573             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
574         raise
575     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
576     comando_ejecutado.fin_tareas = datetime.now() # TODO debería rodear solo la ejecución del comando
577     retorno = self.retorno
578     if retorno == self.RET_PRUEBA:                # FIXME Esto es propio de ComandoPrueba
579         retorno = caso_de_prueba.retorno   # FIXME Esto es propio de ComandoPrueba
580     if retorno != self.RET_ANY:
581         if retorno == self.RET_FAIL:
582             if proc.returncode == 0:
583                 if self.rechazar_si_falla:
584                     prueba.exito = False
585                 comando_ejecutado.exito = False
586                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
587                     u'programa termine con un error (código de retorno '
588                     u'distinto de 0) pero terminó bien (código de retorno '
589                     u'0).\n')
590                 log.debug(_(u'Se esperaba que el programa termine '
591                     u'con un error (código de retorno distinto de 0) pero '
592                     u'terminó bien (código de retorno 0).\n'))
593         elif retorno != proc.returncode:
594             if self.rechazar_si_falla:
595                 prueba.exito = False
596             comando_ejecutado.exito = False
597             if proc.returncode < 0:
598                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
599                     u'con un código de retorno %s pero se obtuvo una señal %s '
600                     u'(%s).\n') % (retorno, -proc.returncode, -proc.returncode) # TODO poner con texto
601                 log.debug(_(u'Se esperaba terminar con un código '
602                     u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
603                     retorno, -proc.returncode, -proc.returncode)
604             else:
605                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
606                     u'con un código de retorno %s pero se obtuvo %s.\n') \
607                     % (retorno, proc.returncode)
608                 log.debug(_(u'Se esperaba terminar con un código de retorno '
609                     u'%s pero se obtuvo %s.\n'), retorno, proc.returncode)
610     if comando_ejecutado.exito is None:
611         log.debug(_(u'Código de retorno OK'))
612     if a_guardar:
613         buffer = StringIO()
614         zip = ZipFile(buffer, 'w')
615         # Guardamos stdout/stderr
616         if self.STDOUTERR in a_guardar:
617             a_guardar.remove(self.STDOUTERR)
618             zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
619                 self.STDOUTERR)
620         else:
621             if self.STDOUT in a_guardar:
622                 a_guardar.remove(self.STDOUT)
623                 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
624                     self.STDOUT)
625             if self.STDERR in a_guardar:
626                 a_guardar.remove(self.STDERR)
627                 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
628                     self.STDERR)
629         # Guardamos otros
630         for f in a_guardar:
631             if not os.path.exists(join(path, f)):
632                 if self.rechazar_si_falla:
633                     prueba.exito = False
634                 comando_ejecutado.exito = False
635                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
636                     u'"%s" para guardar pero no fue encontrado.\n') % f
637                 log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
638                     u'no fue encontrado'), f)
639             else:
640                 zip.write(join(path, f), f)
641         zip.close()
642         comando_ejecutado.archivos = buffer.getvalue()
643     def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
644              newname='entregado'):
645         if longname is None:
646             longname = name
647         new = file(new, 'r').readlines()
648         orig = zip_in.read(name).split('\n')
649         udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
650             tofile=name+'.'+newname)))
651         if udiff:
652             if self.rechazar_si_falla:
653                 prueba.exito = False
654             comando_ejecutado.exito = False
655             comando_ejecutado.observaciones += _(u'%s no coincide con lo '
656                 u'esperado (archivo "%s.diff").\n') % (longname, name)
657             log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
658                 longname, name)
659             htmldiff = HtmlDiff().make_file(orig, new,
660                 fromdesc=name+'.'+origname, todesc=name+'.'+newname,
661                 context=True, numlines=3)
662             zip_out.writestr(name + '.diff', udiff)
663             zip_out.writestr(name + '.diff.html', htmldiff)
664             return True
665         else:
666             return False
667     if a_comparar:
668         buffer = StringIO()
669         zip = ZipFile(buffer, 'w')
670         # Comparamos stdout/stderr
671         if self.STDOUTERR in a_comparar:
672             a_comparar.remove(self.STDOUTERR)
673             diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
674                 zip_a_comparar, zip, self.STDOUTERR,
675                 _(u'La salida estándar y de error combinada'))
676         else:
677             if self.STDOUT in a_comparar:
678                 a_comparar.remove(self.STDOUT)
679                 diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
680                     zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
681             if self.STDERR in a_comparar:
682                 a_comparar.remove(self.STDERR)
683                 diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
684                     zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
685         # Comparamos otros
686         for f in a_comparar:
687             if not os.path.exists(join(path, f)):
688                 if self.rechazar_si_falla:
689                     prueba.exito = False
690                 comando_ejecutado.exito = False
691                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
692                     u'"%s" para comparar pero no fue encontrado') % f
693                 log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
694                     u'no fue encontrado'), f)
695             else:
696                 diff(join(path, f), zip_a_comparar, zip, f)
697         zip.close()
698         comando_ejecutado.diferencias = buffer.getvalue()
699     if comando_ejecutado.exito is None:
700         comando_ejecutado.exito = True
701     elif self.terminar_si_falla:
702         raise ExecutionFailure(self)
703
704 ComandoPrueba.ejecutar = ejecutar_comando_prueba
705 #}}}
706