]> git.llucax.com Git - z.facultad/75.52/sercom.git/blob - sercom/tester.py
Bugfix: arreglar comportamiento de CasoDePrueba y Comando ante excepciones y "fallas.
[z.facultad/75.52/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 SecureProcess(object): #{{{
107     default = dict(
108         max_tiempo_cpu      = 120,
109         max_memoria         = 16,
110         max_tam_archivo     = 5,
111         max_cant_archivos   = 5,
112         max_cant_procesos   = 0,
113         max_locks_memoria   = 0,
114     )
115     uid = config.get('sercom.tester.chroot.user', 65534)
116     MB = 1048576
117     # XXX probar! make de un solo archivo lleva nproc=100 y nofile=15
118     def __init__(self, comando, chroot, cwd, close_stdin=False,
119                  close_stdout=False, close_stderr=False):
120         self.comando = comando
121         self.chroot = chroot
122         self.cwd = cwd
123         self.close_stdin = close_stdin
124         self.close_stdout = close_stdout
125         self.close_stderr = close_stderr
126         log.debug(_(u'Proceso segurizado: chroot=%s, cwd=%s, user=%s, cpu=%s, '
127             u'as=%sMiB, fsize=%sMiB, nofile=%s, nproc=%s, memlock=%s'),
128             self.chroot, self.cwd, self.uid, self.max_tiempo_cpu,
129             self.max_memoria, self.max_tam_archivo, self.max_cant_archivos,
130             self.max_cant_procesos, self.max_locks_memoria)
131     def __getattr__(self, name):
132         if getattr(self.comando, name) is not None:
133             return getattr(self.comando, name)
134         return config.get('sercom.tester.limits.' + name, self.default[name])
135     def __call__(self):
136         x2 = lambda x: (x, x)
137         if self.close_stdin:
138             os.close(0)
139         if self.close_stdout:
140             os.close(1)
141         if self.close_stderr:
142             os.close(2)
143         os.chroot(self.chroot)
144         os.chdir(self.cwd)
145         uinfo = UserInfo(self.uid)
146         os.setgid(uinfo.gid)
147         os.setuid(uinfo.uid) # Somos mortales irreversiblemente
148         rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
149         rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
150         rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
151         rsrc.setrlimit(rsrc.RLIMIT_NOFILE, x2(self.max_cant_archivos)) #XXX Obtener de archivos esperados?
152         rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
153         rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
154         rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
155         # Tratamos de forzar un sync para que entre al sleep del padre FIXME
156         import time
157         time.sleep(0)
158 #}}}
159
160 class Tester(object): #{{{
161
162     def __init__(self, name, path, home, queue): #{{{ y properties
163         self.name = name
164         self.path = path
165         self.home = home
166         self.queue = queue
167         # Ahora somos mortales (oid mortales)
168         os.setegid(user_info.gid)
169         os.seteuid(user_info.uid)
170         log.debug(_(u'usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
171             user_info.user, user_info.group, user_info.uid, user_info.gid)
172
173     @property
174     def build_path(self):
175         return join(self.chroot, self.home, 'build')
176
177     @property
178     def test_path(self):
179         return join(self.chroot, self.home, 'test')
180
181     @property
182     def chroot(self):
183         return join(self.path, 'chroot_' + self.name)
184
185     @property
186     def orig_chroot(self):
187         return join(self.path, 'chroot')
188     #}}}
189
190     def run(self): #{{{
191         entrega_id = self.queue.get() # blocking
192         while entrega_id is not None:
193             entrega = Entrega.get(entrega_id)
194             log.debug(_(u'Nueva entrega para probar en tester %s: %s'),
195                 self.name, entrega)
196             self.test(entrega)
197             log.debug(_(u'Fin de pruebas de: %s'), entrega)
198             entrega_id = self.queue.get() # blocking
199     #}}}
200
201     def test(self, entrega): #{{{
202         log.debug(_(u'Tester.test(entrega=%s)'), entrega)
203         entrega.inicio = datetime.now()
204         try:
205             try:
206                 self.setup_chroot(entrega)
207                 self.ejecutar_tareas_fuente(entrega)
208                 self.ejecutar_tareas_prueba(entrega)
209                 self.clean_chroot(entrega)
210             except ExecutionFailure, e:
211                 pass
212             except Exception, e:
213                 if isinstance(e, SystemExit): raise
214                 entrega.observaciones += error_interno
215                 log.exception(_('Hubo una excepcion inesperada')) # FIXME encoding
216             except:
217                 entrega.observaciones += error_interno
218                 log.exception(_('Hubo una excepcion inesperada desconocida')) # FIXME encoding
219         finally:
220             entrega.fin = datetime.now()
221             if entrega.exito is None:
222                 entrega.exito = True
223             if entrega.exito:
224                 log.info(_(u'Entrega correcta: %s'), entrega)
225             else:
226                 log.info(_(u'Entrega incorrecta: %s'), entrega)
227     #}}}
228
229     def setup_chroot(self, entrega): #{{{ y clean_chroot()
230         log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
231         rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
232             '--archive', '--acls', '--delete-during', '--force', # TODO config
233             join(self.orig_chroot, ''), self.chroot)
234         log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
235         os.seteuid(0) # Dios! (para chroot)
236         os.setegid(0)
237         try:
238             sp.check_call(rsync)
239         finally:
240             os.setegid(user_info.gid) # Mortal de nuevo
241             os.seteuid(user_info.uid)
242             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
243                 user_info.user, user_info.group, user_info.uid, user_info.gid)
244         unzip(entrega.archivos, self.build_path)
245
246     def clean_chroot(self, entrega):
247         log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
248         pass # Se limpia con el próximo rsync
249     #}}}
250
251     def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
252         log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
253             entrega.shortrepr())
254         tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
255                     if isinstance(t, TareaFuente)]
256         for tarea in tareas:
257             tarea.ejecutar(self.build_path, entrega)
258
259     def ejecutar_tareas_prueba(self, entrega):
260         log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
261             entrega.shortrepr())
262         for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
263             caso.ejecutar(self.test_path, entrega)
264     #}}}
265
266 #}}}
267
268 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
269     log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
270         entrega.shortrepr())
271     tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
272                 if isinstance(t, TareaPrueba)]
273     prueba = entrega.add_prueba(self)
274     try:
275         try:
276             for tarea in tareas:
277                 tarea.ejecutar(path, prueba)
278         except ExecutionFailure, e:
279             pass
280     finally:
281         prueba.fin = datetime.now()
282         if prueba.exito is None:
283             prueba.exito = True
284     if not prueba.exito and self.rechazar_si_falla:
285         entrega.exito = False
286     if not prueba.exito and self.terminar_si_falla:
287         raise ExecutionFailure(prueba)
288 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
289 #}}}
290
291 def ejecutar_tarea(self, path, ejecucion): #{{{
292     log.debug(_(u'Tarea.ejecutar(path=%s, ejecucion=%s)'), path,
293         ejecucion.shortrepr())
294     for cmd in self.comandos:
295         cmd.ejecutar(path, ejecucion)
296 Tarea.ejecutar = ejecutar_tarea
297 #}}}
298
299 # TODO generalizar ejecutar_comando_xxxx!!!
300
301 def ejecutar_comando_fuente(self, path, entrega): #{{{
302     log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
303         entrega.shortrepr())
304     comando_ejecutado = entrega.add_comando_ejecutado(self) # TODO debería rodear solo la ejecución del comando
305     basetmp = '/tmp/sercom.tester.fuente' # FIXME TODO /var/run/sercom?
306     unzip(self.archivos_entrada, path, # TODO try/except
307         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
308     options = dict(
309         close_fds=True,
310         shell=True,
311         preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build') #FIXME!!! path
312     )
313     if os.path.exists('%s.%s.stdin' % (basetmp, comando_ejecutado.id)):
314         options['stdin'] = file('%s.%s.stdin' % (basetmp, comando_ejecutado.id),
315             'r')
316     else:
317         options['preexec_fn'].close_stdin = True
318     a_guardar = set(self.archivos_a_guardar)
319     if self.archivos_a_comparar:
320         zip_a_comparar = ZipFile(StringIO(self.archivos_a_comparar), 'r')
321         a_comparar = set(zip_a_comparar.namelist())
322     else:
323         zip_a_comparar = None
324         a_comparar = frozenset()
325     a_usar = frozenset(a_guardar | a_comparar)
326     if self.STDOUTERR in a_usar:
327         options['stdout'] = file('%s.%s.stdouterr' % (basetmp,
328             comando_ejecutado.id), 'w')
329         options['stderr'] = sp.STDOUT
330     else:
331         if self.STDOUT in a_usar:
332             options['stdout'] = file('%s.%s.stdout' % (basetmp,
333                 comando_ejecutado.id), 'w')
334         else:
335             options['preexec_fn'].close_stdout = True
336         if self.STDERR in a_usar:
337             options['stderr'] = file('%s.%s.stderr' % (basetmp,
338                 comando_ejecutado.id), 'w')
339         else:
340             options['preexec_fn'].close_stderr = True
341     log.debug(_(u'Ejecutando como root: %s'), self.comando)
342     os.seteuid(0) # Dios! (para chroot)
343     os.setegid(0)
344     try:
345         try:
346             proc = sp.Popen(self.comando, **options)
347         finally:
348             os.setegid(user_info.gid) # Mortal de nuevo
349             os.seteuid(user_info.uid)
350             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
351                 user_info.user, user_info.group, user_info.uid, user_info.gid)
352     except Exception, e:
353         if hasattr(e, 'child_traceback'):
354             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
355         raise
356     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
357     comando_ejecutado.fin = datetime.now() # TODO debería rodear solo la ejecución del comando
358     if self.retorno != self.RET_ANY:
359         if self.retorno == self.RET_FAIL:
360             if proc.returncode == 0:
361                 if self.rechazar_si_falla:
362                     entrega.exito = False
363                 comando_ejecutado.exito = False
364                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
365                     u'programa termine con un error (código de retorno '
366                     u'distinto de 0) pero terminó bien (código de retorno '
367                     u'0).\n')
368                 log.debug(_(u'Se esperaba que el programa termine '
369                     u'con un error (código de retorno distinto de 0) pero '
370                     u'terminó bien (código de retorno 0).\n'))
371         elif self.retorno != proc.returncode:
372             if self.rechazar_si_falla:
373                 entrega.exito = False
374             comando_ejecutado.exito = False
375             if proc.returncode < 0:
376                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
377                     u'con un código de retorno %s pero se obtuvo una señal %s '
378                     u'(%s).\n') % (self.retorno, -proc.returncode,
379                         -proc.returncode) # TODO poner con texto
380                 log.debug(_(u'Se esperaba terminar con un código '
381                     u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
382                     self.retorno, -proc.returncode, -proc.returncode)
383             else:
384                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
385                     u'con un código de retorno %s pero se obtuvo %s.\n') \
386                     % (self.retorno, proc.returncode)
387                 log.debug(_(u'Se esperaba terminar con un código de retorno '
388                     u'%s pero se obtuvo %s.\n'), self.retorno, proc.returncode)
389     if comando_ejecutado.exito is None:
390         log.debug(_(u'Código de retorno OK'))
391     if a_guardar:
392         buffer = StringIO()
393         zip = ZipFile(buffer, 'w')
394         # Guardamos stdout/stderr
395         if self.STDOUTERR in a_guardar:
396             a_guardar.remove(self.STDOUTERR)
397             zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
398                 self.STDOUTERR)
399         else:
400             if self.STDOUT in a_guardar:
401                 a_guardar.remove(self.STDOUT)
402                 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
403                     self.STDOUT)
404             if self.STDERR in a_guardar:
405                 a_guardar.remove(self.STDERR)
406                 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
407                     self.STDERR)
408         # Guardamos otros
409         for f in a_guardar:
410             if not os.path.exists(join(path, f)):
411                 if self.rechazar_si_falla:
412                     entrega.exito = False
413                 comando_ejecutado.exito = False
414                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
415                     u'"%s" para guardar pero no fue encontrado.\n') % f
416                 log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
417                     u'no fue encontrado'), f)
418             else:
419                 zip.write(join(path, f), f)
420         zip.close()
421         comando_ejecutado.archivos = buffer.getvalue()
422     def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
423              newname='entregado'):
424         if longname is None:
425             longname = name
426         new = file(new, 'r').readlines()
427         orig = zip_in.read(name).split('\n')
428         udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
429             tofile=name+'.'+newname)))
430         if udiff:
431             if self.rechazar_si_falla:
432                 entrega.exito = False
433             comando_ejecutado.exito = False
434             comando_ejecutado.observaciones += _(u'%s no coincide con lo '
435                 u'esperado (archivo "%s.diff").\n') % (longname, name)
436             log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
437                 longname, name)
438             htmldiff = HtmlDiff().make_file(orig, new,
439                 fromdesc=name+'.'+origname, todesc=name+'.'+newname,
440                 context=True, numlines=3)
441             zip_out.writestr(name + '.diff', udiff)
442             zip_out.writestr(name + '.diff.html', htmldiff)
443             return True
444         else:
445             return False
446     if a_comparar:
447         buffer = StringIO()
448         zip = ZipFile(buffer, 'w')
449         # Comparamos stdout/stderr
450         if self.STDOUTERR in a_comparar:
451             a_comparar.remove(self.STDOUTERR)
452             diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
453                 zip_a_comparar, zip, self.STDOUTERR,
454                 _(u'La salida estándar y de error combinada'))
455         else:
456             if self.STDOUT in a_comparar:
457                 a_comparar.remove(self.STDOUT)
458                 diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
459                     zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
460             if self.STDERR in a_comparar:
461                 a_comparar.remove(self.STDERR)
462                 diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
463                     zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
464         # Comparamos otros
465         for f in a_comparar:
466             if not os.path.exists(join(path, f)):
467                 if self.rechazar_si_falla:
468                     entrega.exito = False
469                 comando_ejecutado.exito = False
470                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
471                     u'"%s" para comparar pero no fue encontrado') % f
472                 log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
473                     u'no fue encontrado'), f)
474             else:
475                 diff(join(path, f), zip_a_comparar, zip, f)
476         zip.close()
477         comando_ejecutado.diferencias = buffer.getvalue()
478     if comando_ejecutado.exito is None:
479         comando_ejecutado.exito = True
480     elif self.terminar_si_falla:
481         raise ExecutionFailure(self)
482
483 ComandoFuente.ejecutar = ejecutar_comando_fuente
484 #}}}
485
486 def ejecutar_comando_prueba(self, path, prueba): #{{{
487     # Diferencia con comando fuente: s/entrega/prueba/ y s/build/test/ en path
488     # y setup/clean de test.
489     log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
490         prueba.shortrepr())
491     comando_ejecutado = prueba.add_comando_ejecutado(self) # TODO debería rodear solo la ejecución del comando
492     basetmp = '/tmp/sercom.tester.prueba' # FIXME TODO /var/run/sercom?
493     #{{{ Código que solo va en ComandoPrueba (setup de directorio)
494     rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
495         '--archive', '--acls', '--delete-during', '--force', # TODO config
496         'var/chroot_pepe/home/sercom/build/', path) # FIXME!!!! path
497     log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
498     os.seteuid(0) # Dios! (para chroot)
499     os.setegid(0)
500     try:
501         sp.check_call(rsync)
502     finally:
503         os.setegid(user_info.gid) # Mortal de nuevo
504         os.seteuid(user_info.uid)
505         log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
506             user_info.user, user_info.group, user_info.uid, user_info.gid)
507     unzip(prueba.caso_de_prueba.archivos_entrada, path, # TODO try/except
508         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
509     #}}}
510     unzip(self.archivos_entrada, path, # TODO try/except
511         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
512     options = dict(
513         close_fds=True,
514         shell=True,
515         preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/test') # FIXME!!!! path
516     )
517     if os.path.exists('%s.%s.stdin' % (basetmp, comando_ejecutado.id)):
518         options['stdin'] = file('%s.%s.stdin' % (basetmp, comando_ejecutado.id),
519             'r')
520     else:
521         options['preexec_fn'].close_stdin = True
522     a_guardar = set(self.archivos_a_guardar)
523     if self.archivos_a_comparar:
524         zip_a_comparar = ZipFile(StringIO(self.archivos_a_comparar), 'r')
525         a_comparar = set(zip_a_comparar.namelist())
526     else:
527         zip_a_comparar = None
528         a_comparar = frozenset()
529     a_usar = frozenset(a_guardar | a_comparar)
530     if self.STDOUTERR in a_usar:
531         options['stdout'] = file('%s.%s.stdouterr' % (basetmp,
532             comando_ejecutado.id), 'w')
533         options['stderr'] = sp.STDOUT
534     else:
535         if self.STDOUT in a_usar:
536             options['stdout'] = file('%s.%s.stdout' % (basetmp,
537                 comando_ejecutado.id), 'w')
538         else:
539             options['preexec_fn'].close_stdout = True
540         if self.STDERR in a_usar:
541             options['stderr'] = file('%s.%s.stderr' % (basetmp,
542                 comando_ejecutado.id), 'w')
543         else:
544             options['preexec_fn'].close_stderr = True
545     log.debug(_(u'Ejecutando como root: %s'), self.comando)
546     os.seteuid(0) # Dios! (para chroot)
547     os.setegid(0)
548     try:
549         try:
550             proc = sp.Popen(self.comando, **options)
551         finally:
552             os.setegid(user_info.gid) # Mortal de nuevo
553             os.seteuid(user_info.uid)
554             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
555                 user_info.user, user_info.group, user_info.uid, user_info.gid)
556     except Exception, e:
557         if hasattr(e, 'child_traceback'):
558             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
559         raise
560     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
561     comando_ejecutado.fin_tareas = datetime.now() # TODO debería rodear solo la ejecución del comando
562     if self.retorno != self.RET_ANY:
563         if self.retorno == self.RET_FAIL:
564             if proc.returncode == 0:
565                 if self.rechazar_si_falla:
566                     prueba.exito = False
567                 comando_ejecutado.exito = False
568                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
569                     u'programa termine con un error (código de retorno '
570                     u'distinto de 0) pero terminó bien (código de retorno '
571                     u'0).\n')
572                 log.debug(_(u'Se esperaba que el programa termine '
573                     u'con un error (código de retorno distinto de 0) pero '
574                     u'terminó bien (código de retorno 0).\n'))
575         elif self.retorno != proc.returncode:
576             if self.rechazar_si_falla:
577                 prueba.exito = False
578             comando_ejecutado.exito = False
579             if proc.returncode < 0:
580                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
581                     u'con un código de retorno %s pero se obtuvo una señal %s '
582                     u'(%s).\n') % (self.retorno, -proc.returncode,
583                         -proc.returncode) # TODO poner con texto
584                 log.debug(_(u'Se esperaba terminar con un código '
585                     u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
586                     self.retorno, -proc.returncode, -proc.returncode)
587             else:
588                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
589                     u'con un código de retorno %s pero se obtuvo %s.\n') \
590                     % (self.retorno, proc.returncode)
591                 log.debug(_(u'Se esperaba terminar con un código de retorno '
592                     u'%s pero se obtuvo %s.\n'), self.retorno, proc.returncode)
593     if comando_ejecutado.exito is None:
594         log.debug(_(u'Código de retorno OK'))
595     if a_guardar:
596         buffer = StringIO()
597         zip = ZipFile(buffer, 'w')
598         # Guardamos stdout/stderr
599         if self.STDOUTERR in a_guardar:
600             a_guardar.remove(self.STDOUTERR)
601             zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
602                 self.STDOUTERR)
603         else:
604             if self.STDOUT in a_guardar:
605                 a_guardar.remove(self.STDOUT)
606                 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
607                     self.STDOUT)
608             if self.STDERR in a_guardar:
609                 a_guardar.remove(self.STDERR)
610                 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
611                     self.STDERR)
612         # Guardamos otros
613         for f in a_guardar:
614             if not os.path.exists(join(path, f)):
615                 if self.rechazar_si_falla:
616                     prueba.exito = False
617                 comando_ejecutado.exito = False
618                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
619                     u'"%s" para guardar pero no fue encontrado.\n') % f
620                 log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
621                     u'no fue encontrado'), f)
622             else:
623                 zip.write(join(path, f), f)
624         zip.close()
625         comando_ejecutado.archivos = buffer.getvalue()
626     def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
627              newname='entregado'):
628         if longname is None:
629             longname = name
630         new = file(new, 'r').readlines()
631         orig = zip_in.read(name).split('\n')
632         udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
633             tofile=name+'.'+newname)))
634         if udiff:
635             if self.rechazar_si_falla:
636                 prueba.exito = False
637             comando_ejecutado.exito = False
638             comando_ejecutado.observaciones += _(u'%s no coincide con lo '
639                 u'esperado (archivo "%s.diff").\n') % (longname, name)
640             log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
641                 longname, name)
642             htmldiff = HtmlDiff().make_file(orig, new,
643                 fromdesc=name+'.'+origname, todesc=name+'.'+newname,
644                 context=True, numlines=3)
645             zip_out.writestr(name + '.diff', udiff)
646             zip_out.writestr(name + '.diff.html', htmldiff)
647             return True
648         else:
649             return False
650     if a_comparar:
651         buffer = StringIO()
652         zip = ZipFile(buffer, 'w')
653         # Comparamos stdout/stderr
654         if self.STDOUTERR in a_comparar:
655             a_comparar.remove(self.STDOUTERR)
656             diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
657                 zip_a_comparar, zip, self.STDOUTERR,
658                 _(u'La salida estándar y de error combinada'))
659         else:
660             if self.STDOUT in a_comparar:
661                 a_comparar.remove(self.STDOUT)
662                 diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
663                     zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
664             if self.STDERR in a_comparar:
665                 a_comparar.remove(self.STDERR)
666                 diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
667                     zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
668         # Comparamos otros
669         for f in a_comparar:
670             if not os.path.exists(join(path, f)):
671                 if self.rechazar_si_falla:
672                     prueba.exito = False
673                 comando_ejecutado.exito = False
674                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
675                     u'"%s" para comparar pero no fue encontrado') % f
676                 log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
677                     u'no fue encontrado'), f)
678             else:
679                 diff(join(path, f), zip_a_comparar, zip, f)
680         zip.close()
681         comando_ejecutado.diferencias = buffer.getvalue()
682     if comando_ejecutado.exito is None:
683         comando_ejecutado.exito = True
684     elif self.terminar_si_falla:
685         raise ExecutionFailure(self)
686
687 ComandoPrueba.ejecutar = ejecutar_comando_prueba
688 #}}}
689