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