]> git.llucax.com Git - z.facultad/75.52/sercom.git/blob - sercom/tester.py
Bugfix: tener en cuenta si ComandoPrueba.retorno es ComandoPrueba.RET_PRUEBA.
[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     retorno = self.retorno
359     if retorno != self.RET_ANY:
360         if retorno == self.RET_FAIL:
361             if proc.returncode == 0:
362                 if self.rechazar_si_falla:
363                     entrega.exito = False
364                 comando_ejecutado.exito = False
365                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
366                     u'programa termine con un error (código de retorno '
367                     u'distinto de 0) pero terminó bien (código de retorno '
368                     u'0).\n')
369                 log.debug(_(u'Se esperaba que el programa termine '
370                     u'con un error (código de retorno distinto de 0) pero '
371                     u'terminó bien (código de retorno 0).\n'))
372         elif retorno != proc.returncode:
373             if self.rechazar_si_falla:
374                 entrega.exito = False
375             comando_ejecutado.exito = False
376             if proc.returncode < 0:
377                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
378                     u'con un código de retorno %s pero se obtuvo una señal %s '
379                     u'(%s).\n') % (retorno, -proc.returncode, -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                     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                     % (retorno, proc.returncode)
387                 log.debug(_(u'Se esperaba terminar con un código de retorno '
388                     u'%s pero se obtuvo %s.\n'), 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     retorno = self.retorno
563     if retorno == self.RET_PRUEBA:                # FIXME Esto es propio de ComandoPrueba
564         retorno = prueba.caso_de_prueba.retorno   # FIXME Esto es propio de ComandoPrueba
565     if retorno != self.RET_ANY:
566         if retorno == self.RET_FAIL:
567             if proc.returncode == 0:
568                 if self.rechazar_si_falla:
569                     prueba.exito = False
570                 comando_ejecutado.exito = False
571                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
572                     u'programa termine con un error (código de retorno '
573                     u'distinto de 0) pero terminó bien (código de retorno '
574                     u'0).\n')
575                 log.debug(_(u'Se esperaba que el programa termine '
576                     u'con un error (código de retorno distinto de 0) pero '
577                     u'terminó bien (código de retorno 0).\n'))
578         elif retorno != proc.returncode:
579             if self.rechazar_si_falla:
580                 prueba.exito = False
581             comando_ejecutado.exito = False
582             if proc.returncode < 0:
583                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
584                     u'con un código de retorno %s pero se obtuvo una señal %s '
585                     u'(%s).\n') % (retorno, -proc.returncode, -proc.returncode) # TODO poner con texto
586                 log.debug(_(u'Se esperaba terminar con un código '
587                     u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
588                     retorno, -proc.returncode, -proc.returncode)
589             else:
590                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
591                     u'con un código de retorno %s pero se obtuvo %s.\n') \
592                     % (retorno, proc.returncode)
593                 log.debug(_(u'Se esperaba terminar con un código de retorno '
594                     u'%s pero se obtuvo %s.\n'), retorno, proc.returncode)
595     if comando_ejecutado.exito is None:
596         log.debug(_(u'Código de retorno OK'))
597     if a_guardar:
598         buffer = StringIO()
599         zip = ZipFile(buffer, 'w')
600         # Guardamos stdout/stderr
601         if self.STDOUTERR in a_guardar:
602             a_guardar.remove(self.STDOUTERR)
603             zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
604                 self.STDOUTERR)
605         else:
606             if self.STDOUT in a_guardar:
607                 a_guardar.remove(self.STDOUT)
608                 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
609                     self.STDOUT)
610             if self.STDERR in a_guardar:
611                 a_guardar.remove(self.STDERR)
612                 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
613                     self.STDERR)
614         # Guardamos otros
615         for f in a_guardar:
616             if not os.path.exists(join(path, f)):
617                 if self.rechazar_si_falla:
618                     prueba.exito = False
619                 comando_ejecutado.exito = False
620                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
621                     u'"%s" para guardar pero no fue encontrado.\n') % f
622                 log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
623                     u'no fue encontrado'), f)
624             else:
625                 zip.write(join(path, f), f)
626         zip.close()
627         comando_ejecutado.archivos = buffer.getvalue()
628     def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
629              newname='entregado'):
630         if longname is None:
631             longname = name
632         new = file(new, 'r').readlines()
633         orig = zip_in.read(name).split('\n')
634         udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
635             tofile=name+'.'+newname)))
636         if udiff:
637             if self.rechazar_si_falla:
638                 prueba.exito = False
639             comando_ejecutado.exito = False
640             comando_ejecutado.observaciones += _(u'%s no coincide con lo '
641                 u'esperado (archivo "%s.diff").\n') % (longname, name)
642             log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
643                 longname, name)
644             htmldiff = HtmlDiff().make_file(orig, new,
645                 fromdesc=name+'.'+origname, todesc=name+'.'+newname,
646                 context=True, numlines=3)
647             zip_out.writestr(name + '.diff', udiff)
648             zip_out.writestr(name + '.diff.html', htmldiff)
649             return True
650         else:
651             return False
652     if a_comparar:
653         buffer = StringIO()
654         zip = ZipFile(buffer, 'w')
655         # Comparamos stdout/stderr
656         if self.STDOUTERR in a_comparar:
657             a_comparar.remove(self.STDOUTERR)
658             diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
659                 zip_a_comparar, zip, self.STDOUTERR,
660                 _(u'La salida estándar y de error combinada'))
661         else:
662             if self.STDOUT in a_comparar:
663                 a_comparar.remove(self.STDOUT)
664                 diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
665                     zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
666             if self.STDERR in a_comparar:
667                 a_comparar.remove(self.STDERR)
668                 diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
669                     zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
670         # Comparamos otros
671         for f in a_comparar:
672             if not os.path.exists(join(path, f)):
673                 if self.rechazar_si_falla:
674                     prueba.exito = False
675                 comando_ejecutado.exito = False
676                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
677                     u'"%s" para comparar pero no fue encontrado') % f
678                 log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
679                     u'no fue encontrado'), f)
680             else:
681                 diff(join(path, f), zip_a_comparar, zip, f)
682         zip.close()
683         comando_ejecutado.diferencias = buffer.getvalue()
684     if comando_ejecutado.exito is None:
685         comando_ejecutado.exito = True
686     elif self.terminar_si_falla:
687         raise ExecutionFailure(self)
688
689 ComandoPrueba.ejecutar = ejecutar_comando_prueba
690 #}}}
691