]> git.llucax.com Git - software/sercom.git/blob - sercom/tester.py
abc1d9052f9d7394d4646ff40300967b182739b8
[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         buffer = StringIO()
465         zip = ZipFile(buffer, 'w')
466         # Comparamos stdout/stderr
467         if self.STDOUTERR in a_comparar:
468             a_comparar.remove(self.STDOUTERR)
469             diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
470                 zip_a_comparar, zip, self.STDOUTERR,
471                 _(u'La salida estándar y de error combinada'))
472         else:
473             if self.STDOUT in a_comparar:
474                 a_comparar.remove(self.STDOUT)
475                 diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
476                     zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
477             if self.STDERR in a_comparar:
478                 a_comparar.remove(self.STDERR)
479                 diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
480                     zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
481         # Comparamos otros
482         for f in a_comparar:
483             if not os.path.exists(join(path, f)):
484                 if self.rechazar_si_falla:
485                     entrega.exito = False
486                 comando_ejecutado.exito = False
487                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
488                     u'"%s" para comparar pero no fue encontrado') % f
489                 log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
490                     u'no fue encontrado'), f)
491             else:
492                 diff(join(path, f), zip_a_comparar, zip, f)
493         zip.close()
494         comando_ejecutado.diferencias = buffer.getvalue()
495     if comando_ejecutado.exito is None:
496         comando_ejecutado.exito = True
497     elif self.terminar_si_falla:
498         raise ExecutionFailure(self)
499
500 ComandoFuente.ejecutar = ejecutar_comando_fuente
501 #}}}
502
503 def ejecutar_comando_prueba(self, path, prueba): #{{{
504     # Diferencia con comando fuente: s/entrega/prueba/ y s/build/test/ en path
505     # y setup/clean de test.
506     log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
507         prueba.shortrepr())
508     caso_de_prueba = prueba.caso_de_prueba
509     comando_ejecutado = prueba.add_comando_ejecutado(self)
510     basetmp = '/tmp/sercom.tester.prueba' # FIXME TODO /var/run/sercom?
511     #{{{ Código que solo va en ComandoPrueba (setup de directorio)
512     rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
513         '--archive', '--acls', '--delete-during', '--force', # TODO config
514         'var/chroot_pepe/home/sercom/build/', path) # FIXME!!!! path
515     log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
516     os.seteuid(0) # Dios! (para chroot)
517     os.setegid(0)
518     try:
519         sp.check_call(rsync)
520     finally:
521         os.setegid(user_info.gid) # Mortal de nuevo
522         os.seteuid(user_info.uid)
523         log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
524             user_info.user, user_info.group, user_info.uid, user_info.gid)
525     #}}}
526     unzip(self.archivos_entrada, path, # TODO try/except
527         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
528     unzip(caso_de_prueba.archivos_entrada, path, # TODO try/except     # FIXME Esto es propio de ComandoPrueba
529         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)}) # FIXME Esto es propio de ComandoPrueba
530     options = dict(
531         close_fds=True,
532         shell=True,
533         preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/test') # FIXME!!!! path
534     )
535     if os.path.exists('%s.%s.stdin' % (basetmp, comando_ejecutado.id)):
536         options['stdin'] = file('%s.%s.stdin' % (basetmp, comando_ejecutado.id),
537             'r')
538     else:
539         options['preexec_fn'].close_stdin = True
540     a_guardar = set(self.archivos_a_guardar)
541     a_guardar |= set(caso_de_prueba.archivos_a_guardar)           # FIXME Esto es propio de ComandoPrueba
542     log.debug('archivos a guardar: %s', a_guardar)
543     zip_a_comparar = Multizip(caso_de_prueba.archivos_a_comparar, # FIXME Esto es propio de ComandoPrueba
544         self.archivos_a_comparar)                                 # FIXME Esto es propio de ComandoPrueba
545     a_comparar = set(zip_a_comparar.namelist())
546     log.debug('archivos a comparar: %s', a_comparar)
547     a_usar = frozenset(a_guardar | a_comparar)
548     log.debug('archivos a usar: %s', a_usar)
549     if self.STDOUTERR in a_usar:
550         options['stdout'] = file('%s.%s.stdouterr' % (basetmp,
551             comando_ejecutado.id), 'w')
552         options['stderr'] = sp.STDOUT
553     else:
554         if self.STDOUT in a_usar:
555             log.debug('capurando salida en: %s.%s.stdout', basetmp, comando_ejecutado.id)
556             options['stdout'] = file('%s.%s.stdout' % (basetmp,
557                 comando_ejecutado.id), 'w')
558         else:
559             options['preexec_fn'].close_stdout = True
560         if self.STDERR in a_usar:
561             options['stderr'] = file('%s.%s.stderr' % (basetmp,
562                 comando_ejecutado.id), 'w')
563         else:
564             options['preexec_fn'].close_stderr = True
565     comando = self.comando + ' ' + caso_de_prueba.comando # FIXME Esto es propio de ComandoPrueba
566     comando_ejecutado.inicio = datetime.now()
567     log.debug(_(u'Ejecutando como root: %s'), comando)
568     os.seteuid(0) # Dios! (para chroot)
569     os.setegid(0)
570     try:
571         try:
572             proc = sp.Popen(comando, **options)
573         finally:
574             os.setegid(user_info.gid) # Mortal de nuevo
575             os.seteuid(user_info.uid)
576             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
577                 user_info.user, user_info.group, user_info.uid, user_info.gid)
578     except Exception, e:
579         if hasattr(e, 'child_traceback'):
580             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
581         raise
582     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
583     comando_ejecutado.fin = datetime.now()
584     retorno = self.retorno
585     if retorno == self.RET_PRUEBA:                # FIXME Esto es propio de ComandoPrueba
586         retorno = caso_de_prueba.retorno   # FIXME Esto es propio de ComandoPrueba
587     if retorno != self.RET_ANY:
588         if retorno == self.RET_FAIL:
589             if proc.returncode == 0:
590                 if self.rechazar_si_falla:
591                     prueba.exito = False
592                 comando_ejecutado.exito = False
593                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
594                     u'programa termine con un error (código de retorno '
595                     u'distinto de 0) pero terminó bien (código de retorno '
596                     u'0).\n')
597                 log.debug(_(u'Se esperaba que el programa termine '
598                     u'con un error (código de retorno distinto de 0) pero '
599                     u'terminó bien (código de retorno 0).\n'))
600         elif retorno != proc.returncode:
601             if self.rechazar_si_falla:
602                 prueba.exito = False
603             comando_ejecutado.exito = False
604             if proc.returncode < 0:
605                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
606                     u'con un código de retorno %s pero se obtuvo una señal %s '
607                     u'(%s).\n') % (retorno, -proc.returncode, -proc.returncode) # TODO poner con texto
608                 log.debug(_(u'Se esperaba terminar con un código '
609                     u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
610                     retorno, -proc.returncode, -proc.returncode)
611             else:
612                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
613                     u'con un código de retorno %s pero se obtuvo %s.\n') \
614                     % (retorno, proc.returncode)
615                 log.debug(_(u'Se esperaba terminar con un código de retorno '
616                     u'%s pero se obtuvo %s.\n'), retorno, proc.returncode)
617     if comando_ejecutado.exito is None:
618         log.debug(_(u'Código de retorno OK'))
619     if a_guardar:
620         buffer = StringIO()
621         zip = ZipFile(buffer, 'w')
622         # Guardamos stdout/stderr
623         if self.STDOUTERR in a_guardar:
624             a_guardar.remove(self.STDOUTERR)
625             zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
626                 self.STDOUTERR)
627         else:
628             if self.STDOUT in a_guardar:
629                 a_guardar.remove(self.STDOUT)
630                 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
631                     self.STDOUT)
632             if self.STDERR in a_guardar:
633                 a_guardar.remove(self.STDERR)
634                 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
635                     self.STDERR)
636         # Guardamos otros
637         for f in a_guardar:
638             if not os.path.exists(join(path, f)):
639                 if self.rechazar_si_falla:
640                     prueba.exito = False
641                 comando_ejecutado.exito = False
642                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
643                     u'"%s" para guardar pero no fue encontrado.\n') % f
644                 log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
645                     u'no fue encontrado'), f)
646             else:
647                 zip.write(join(path, f), f)
648         zip.close()
649         comando_ejecutado.archivos = buffer.getvalue()
650     def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
651              newname='entregado'):
652         if longname is None:
653             longname = name
654         new = file(new, 'r').readlines()
655         orig = zip_in.read(name).splitlines(True)
656         udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
657             tofile=name+'.'+newname)))
658         if udiff:
659             if self.rechazar_si_falla:
660                 prueba.exito = False
661             comando_ejecutado.exito = False
662             comando_ejecutado.observaciones += _(u'%s no coincide con lo '
663                 u'esperado (archivo "%s.diff").\n') % (longname, name)
664             log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
665                 longname, name)
666             htmldiff = HtmlDiff().make_file(orig, new,
667                 fromdesc=name+'.'+origname, todesc=name+'.'+newname,
668                 context=True, numlines=3)
669             zip_out.writestr(name + '.diff', udiff)
670             zip_out.writestr(name + '.html', htmldiff)
671             return True
672         else:
673             return False
674     if a_comparar:
675         buffer = StringIO()
676         zip = ZipFile(buffer, 'w')
677         # Comparamos stdout/stderr
678         if self.STDOUTERR in a_comparar:
679             a_comparar.remove(self.STDOUTERR)
680             diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
681                 zip_a_comparar, zip, self.STDOUTERR,
682                 _(u'La salida estándar y de error combinada'))
683         else:
684             if self.STDOUT in a_comparar:
685                 log.debug('comparando salida con: %s.%s.stdout', basetmp, comando_ejecutado.id)
686                 a_comparar.remove(self.STDOUT)
687                 diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
688                     zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
689             if self.STDERR in a_comparar:
690                 a_comparar.remove(self.STDERR)
691                 diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
692                     zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
693         # Comparamos otros
694         for f in a_comparar:
695             if not os.path.exists(join(path, f)):
696                 if self.rechazar_si_falla:
697                     prueba.exito = False
698                 comando_ejecutado.exito = False
699                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
700                     u'"%s" para comparar pero no fue encontrado') % f
701                 log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
702                     u'no fue encontrado'), f)
703             else:
704                 diff(join(path, f), zip_a_comparar, zip, f)
705         zip.close()
706         comando_ejecutado.diferencias = buffer.getvalue()
707     if comando_ejecutado.exito is None:
708         comando_ejecutado.exito = True
709     elif self.terminar_si_falla:
710         raise ExecutionFailure(self)
711
712 ComandoPrueba.ejecutar = ejecutar_comando_prueba
713 #}}}
714