]> git.llucax.com Git - software/sercom.git/blob - sercom/tester.py
b7b60d1d4d537540d78eecb9f47bba2e1541eba4
[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(path=%s, entrega=%s)'), path,
289         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).split('\n')
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.html', 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     zip_a_comparar = Multizip(caso_de_prueba.archivos_a_comparar, # FIXME Esto es propio de ComandoPrueba
543         self.archivos_a_comparar)                                 # FIXME Esto es propio de ComandoPrueba
544     a_comparar = set(zip_a_comparar.namelist())
545     a_usar = frozenset(a_guardar | a_comparar)
546     if self.STDOUTERR in a_usar:
547         options['stdout'] = file('%s.%s.stdouterr' % (basetmp,
548             comando_ejecutado.id), 'w')
549         options['stderr'] = sp.STDOUT
550     else:
551         if self.STDOUT in a_usar:
552             options['stdout'] = file('%s.%s.stdout' % (basetmp,
553                 comando_ejecutado.id), 'w')
554         else:
555             options['preexec_fn'].close_stdout = True
556         if self.STDERR in a_usar:
557             options['stderr'] = file('%s.%s.stderr' % (basetmp,
558                 comando_ejecutado.id), 'w')
559         else:
560             options['preexec_fn'].close_stderr = True
561     comando = self.comando + ' ' + caso_de_prueba.comando # FIXME Esto es propio de ComandoPrueba
562     comando_ejecutado.inicio = datetime.now()
563     log.debug(_(u'Ejecutando como root: %s'), comando)
564     os.seteuid(0) # Dios! (para chroot)
565     os.setegid(0)
566     try:
567         try:
568             proc = sp.Popen(comando, **options)
569         finally:
570             os.setegid(user_info.gid) # Mortal de nuevo
571             os.seteuid(user_info.uid)
572             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
573                 user_info.user, user_info.group, user_info.uid, user_info.gid)
574     except Exception, e:
575         if hasattr(e, 'child_traceback'):
576             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
577         raise
578     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
579     comando_ejecutado.fin = datetime.now()
580     retorno = self.retorno
581     if retorno == self.RET_PRUEBA:                # FIXME Esto es propio de ComandoPrueba
582         retorno = caso_de_prueba.retorno   # FIXME Esto es propio de ComandoPrueba
583     if retorno != self.RET_ANY:
584         if retorno == self.RET_FAIL:
585             if proc.returncode == 0:
586                 if self.rechazar_si_falla:
587                     prueba.exito = False
588                 comando_ejecutado.exito = False
589                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
590                     u'programa termine con un error (código de retorno '
591                     u'distinto de 0) pero terminó bien (código de retorno '
592                     u'0).\n')
593                 log.debug(_(u'Se esperaba que el programa termine '
594                     u'con un error (código de retorno distinto de 0) pero '
595                     u'terminó bien (código de retorno 0).\n'))
596         elif retorno != proc.returncode:
597             if self.rechazar_si_falla:
598                 prueba.exito = False
599             comando_ejecutado.exito = False
600             if proc.returncode < 0:
601                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
602                     u'con un código de retorno %s pero se obtuvo una señal %s '
603                     u'(%s).\n') % (retorno, -proc.returncode, -proc.returncode) # TODO poner con texto
604                 log.debug(_(u'Se esperaba terminar con un código '
605                     u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
606                     retorno, -proc.returncode, -proc.returncode)
607             else:
608                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
609                     u'con un código de retorno %s pero se obtuvo %s.\n') \
610                     % (retorno, proc.returncode)
611                 log.debug(_(u'Se esperaba terminar con un código de retorno '
612                     u'%s pero se obtuvo %s.\n'), retorno, proc.returncode)
613     if comando_ejecutado.exito is None:
614         log.debug(_(u'Código de retorno OK'))
615     if a_guardar:
616         buffer = StringIO()
617         zip = ZipFile(buffer, 'w')
618         # Guardamos stdout/stderr
619         if self.STDOUTERR in a_guardar:
620             a_guardar.remove(self.STDOUTERR)
621             zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
622                 self.STDOUTERR)
623         else:
624             if self.STDOUT in a_guardar:
625                 a_guardar.remove(self.STDOUT)
626                 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
627                     self.STDOUT)
628             if self.STDERR in a_guardar:
629                 a_guardar.remove(self.STDERR)
630                 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
631                     self.STDERR)
632         # Guardamos otros
633         for f in a_guardar:
634             if not os.path.exists(join(path, f)):
635                 if self.rechazar_si_falla:
636                     prueba.exito = False
637                 comando_ejecutado.exito = False
638                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
639                     u'"%s" para guardar pero no fue encontrado.\n') % f
640                 log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
641                     u'no fue encontrado'), f)
642             else:
643                 zip.write(join(path, f), f)
644         zip.close()
645         comando_ejecutado.archivos = buffer.getvalue()
646     def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
647              newname='entregado'):
648         if longname is None:
649             longname = name
650         new = file(new, 'r').readlines()
651         orig = zip_in.read(name).split('\n')
652         udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
653             tofile=name+'.'+newname)))
654         if udiff:
655             if self.rechazar_si_falla:
656                 prueba.exito = False
657             comando_ejecutado.exito = False
658             comando_ejecutado.observaciones += _(u'%s no coincide con lo '
659                 u'esperado (archivo "%s.diff").\n') % (longname, name)
660             log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
661                 longname, name)
662             htmldiff = HtmlDiff().make_file(orig, new,
663                 fromdesc=name+'.'+origname, todesc=name+'.'+newname,
664                 context=True, numlines=3)
665             zip_out.writestr(name + '.diff', udiff)
666             zip_out.writestr(name + '.diff.html', htmldiff)
667             return True
668         else:
669             return False
670     if a_comparar:
671         buffer = StringIO()
672         zip = ZipFile(buffer, 'w')
673         # Comparamos stdout/stderr
674         if self.STDOUTERR in a_comparar:
675             a_comparar.remove(self.STDOUTERR)
676             diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
677                 zip_a_comparar, zip, self.STDOUTERR,
678                 _(u'La salida estándar y de error combinada'))
679         else:
680             if self.STDOUT in a_comparar:
681                 a_comparar.remove(self.STDOUT)
682                 diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
683                     zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
684             if self.STDERR in a_comparar:
685                 a_comparar.remove(self.STDERR)
686                 diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
687                     zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
688         # Comparamos otros
689         for f in a_comparar:
690             if not os.path.exists(join(path, f)):
691                 if self.rechazar_si_falla:
692                     prueba.exito = False
693                 comando_ejecutado.exito = False
694                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
695                     u'"%s" para comparar pero no fue encontrado') % f
696                 log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
697                     u'no fue encontrado'), f)
698             else:
699                 diff(join(path, f), zip_a_comparar, zip, f)
700         zip.close()
701         comando_ejecutado.diferencias = buffer.getvalue()
702     if comando_ejecutado.exito is None:
703         comando_ejecutado.exito = True
704     elif self.terminar_si_falla:
705         raise ExecutionFailure(self)
706
707 ComandoPrueba.ejecutar = ejecutar_comando_prueba
708 #}}}
709