]> git.llucax.com Git - software/sercom.git/blob - sercom/tester.py
solo muestro miembros activos
[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 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                 entrega.exito = False
212                 log.info(_(u'Entrega incorrecta: %s'), entrega)
213             except Exception, e:
214                 if isinstance(e, SystemExit): raise
215                 entrega.observaciones += error_interno
216                 log.exception(_('Hubo una excepcion inesperada')) # FIXME encoding
217             except:
218                 entrega.observaciones += error_interno
219                 log.exception(_('Hubo una excepcion inesperada desconocida')) # FIXME encoding
220             else:
221                 entrega.exito = True
222                 log.debug(_(u'Entrega correcta: %s'), entrega)
223         finally:
224             entrega.fin = datetime.now()
225     #}}}
226
227     def setup_chroot(self, entrega): #{{{ y clean_chroot()
228         log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
229         rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
230             '--archive', '--acls', '--delete-during', '--force', # TODO config
231             join(self.orig_chroot, ''), self.chroot)
232         log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
233         os.seteuid(0) # Dios! (para chroot)
234         os.setegid(0)
235         try:
236             sp.check_call(rsync)
237         finally:
238             os.setegid(user_info.gid) # Mortal de nuevo
239             os.seteuid(user_info.uid)
240             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
241                 user_info.user, user_info.group, user_info.uid, user_info.gid)
242         unzip(entrega.archivos, self.build_path)
243
244     def clean_chroot(self, entrega):
245         log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
246         pass # Se limpia con el próximo rsync
247     #}}}
248
249     def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
250         log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
251             entrega.shortrepr())
252         tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
253                     if isinstance(t, TareaFuente)]
254         for tarea in tareas:
255             tarea.ejecutar(self.build_path, entrega)
256
257     def ejecutar_tareas_prueba(self, entrega):
258         log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
259             entrega.shortrepr())
260         for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
261             caso.ejecutar(self.test_path, entrega)
262     #}}}
263
264 #}}}
265
266 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
267     log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
268         entrega.shortrepr())
269     tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
270                 if isinstance(t, TareaPrueba)]
271     prueba = entrega.add_prueba(self)
272     try:
273         try:
274             for tarea in tareas:
275                 tarea.ejecutar(path, prueba)
276         except ExecutionFailure, e:
277             prueba.exito = False
278             if self.rechazar_si_falla:
279                 entrega.exito = False
280             if self.terminar_si_falla:
281                 raise ExecutionFailure(e.comando, e.tarea, self)
282         else:
283             prueba.exito = True
284     finally:
285         prueba.fin = datetime.now()
286 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
287 #}}}
288
289 def ejecutar_tarea(self, path, ejecucion): #{{{
290     log.debug(_(u'Tarea.ejecutar(path=%s, ejecucion=%s)'), path,
291         ejecucion.shortrepr())
292     for cmd in self.comandos:
293         cmd.ejecutar(path, ejecucion)
294 Tarea.ejecutar = ejecutar_tarea
295 #}}}
296
297 # TODO generalizar ejecutar_comando_xxxx!!!
298
299 def ejecutar_comando_fuente(self, path, entrega): #{{{
300     log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
301         entrega.shortrepr())
302     comando_ejecutado = entrega.add_comando_ejecutado(self) # TODO debería rodear solo la ejecución del comando
303     basetmp = '/tmp/sercom.tester.fuente' # FIXME TODO /var/run/sercom?
304     unzip(self.archivos_entrada, path, # TODO try/except
305         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
306     options = dict(
307         close_fds=True,
308         shell=True,
309         preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build') #FIXME!!! path
310     )
311     if os.path.exists('%s.%s.stdin' % (basetmp, comando_ejecutado.id)):
312         options['stdin'] = file('%s.%s.stdin' % (basetmp, comando_ejecutado.id),
313             'r')
314     else:
315         options['preexec_fn'].close_stdin = True
316     a_guardar = set(self.archivos_a_guardar)
317     if self.archivos_a_comparar:
318         zip_a_comparar = ZipFile(StringIO(self.archivos_a_comparar), 'r')
319         a_comparar = set(zip_a_comparar.namelist())
320     else:
321         zip_a_comparar = None
322         a_comparar = frozenset()
323     a_usar = frozenset(a_guardar | a_comparar)
324     if self.STDOUTERR in a_usar:
325         options['stdout'] = file('%s.%s.stdouterr' % (basetmp,
326             comando_ejecutado.id), 'w')
327         options['stderr'] = sp.STDOUT
328     else:
329         if self.STDOUT in a_usar:
330             options['stdout'] = file('%s.%s.stdout' % (basetmp,
331                 comando_ejecutado.id), 'w')
332         else:
333             options['preexec_fn'].close_stdout = True
334         if self.STDERR in a_usar:
335             options['stderr'] = file('%s.%s.stderr' % (basetmp,
336                 comando_ejecutado.id), 'w')
337         else:
338             options['preexec_fn'].close_stderr = True
339     log.debug(_(u'Ejecutando como root: %s'), self.comando)
340     os.seteuid(0) # Dios! (para chroot)
341     os.setegid(0)
342     try:
343         try:
344             proc = sp.Popen(self.comando, **options)
345         finally:
346             os.setegid(user_info.gid) # Mortal de nuevo
347             os.seteuid(user_info.uid)
348             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
349                 user_info.user, user_info.group, user_info.uid, user_info.gid)
350     except Exception, e:
351         if hasattr(e, 'child_traceback'):
352             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
353         raise
354     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
355     comando_ejecutado.fin = datetime.now() # TODO debería rodear solo la ejecución del comando
356     if self.retorno != self.RET_ANY:
357         if self.retorno == self.RET_FAIL:
358             if proc.returncode == 0:
359                 if self.rechazar_si_falla:
360                     entrega.exito = False
361                 comando_ejecutado.exito = False
362                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
363                     u'programa termine con un error (código de retorno '
364                     u'distinto de 0) pero terminó bien (código de retorno '
365                     u'0).\n')
366                 log.debug(_(u'Se esperaba que el programa termine '
367                     u'con un error (código de retorno distinto de 0) pero '
368                     u'terminó bien (código de retorno 0).\n'))
369         elif self.retorno != proc.returncode:
370             if self.rechazar_si_falla:
371                 entrega.exito = False
372             comando_ejecutado.exito = False
373             if proc.returncode < 0:
374                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
375                     u'con un código de retorno %s pero se obtuvo una señal %s '
376                     u'(%s).\n') % (self.retorno, -proc.returncode,
377                         -proc.returncode) # TODO poner con texto
378                 log.debug(_(u'Se esperaba terminar con un código '
379                     u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
380                     self.retorno, -proc.returncode, -proc.returncode)
381             else:
382                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
383                     u'con un código de retorno %s pero se obtuvo %s.\n') \
384                     % (self.retorno, proc.returncode)
385                 log.debug(_(u'Se esperaba terminar con un código de retorno '
386                     u'%s pero se obtuvo %s.\n'), self.retorno, proc.returncode)
387     if comando_ejecutado.exito is None:
388         log.debug(_(u'Código de retorno OK'))
389     if a_guardar:
390         buffer = StringIO()
391         zip = ZipFile(buffer, 'w')
392         # Guardamos stdout/stderr
393         if self.STDOUTERR in a_guardar:
394             a_guardar.remove(self.STDOUTERR)
395             zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
396                 self.STDOUTERR)
397         else:
398             if self.STDOUT in a_guardar:
399                 a_guardar.remove(self.STDOUT)
400                 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
401                     self.STDOUT)
402             if self.STDERR in a_guardar:
403                 a_guardar.remove(self.STDERR)
404                 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
405                     self.STDERR)
406         # Guardamos otros
407         for f in a_guardar:
408             if not os.path.exists(join(path, f)):
409                 if self.rechazar_si_falla:
410                     entrega.exito = False
411                 comando_ejecutado.exito = False
412                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
413                     u'"%s" para guardar pero no fue encontrado.\n') % f
414                 log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
415                     u'no fue encontrado'), f)
416             else:
417                 zip.write(join(path, f), f)
418         zip.close()
419         comando_ejecutado.archivos = buffer.getvalue()
420     def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
421              newname='entregado'):
422         if longname is None:
423             longname = name
424         new = file(new, 'r').readlines()
425         orig = zip_in.read(name).split('\n')
426         udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
427             tofile=name+'.'+newname)))
428         if udiff:
429             if self.rechazar_si_falla:
430                 entrega.exito = False
431             comando_ejecutado.exito = False
432             comando_ejecutado.observaciones += _(u'%s no coincide con lo '
433                 u'esperado (archivo "%s.diff").\n') % (longname, name)
434             log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
435                 longname, name)
436             htmldiff = HtmlDiff().make_file(orig, new,
437                 fromdesc=name+'.'+origname, todesc=name+'.'+newname,
438                 context=True, numlines=3)
439             zip_out.writestr(name + '.diff', udiff)
440             zip_out.writestr(name + '.diff.html', htmldiff)
441             return True
442         else:
443             return False
444     if a_comparar:
445         buffer = StringIO()
446         zip = ZipFile(buffer, 'w')
447         # Comparamos stdout/stderr
448         if self.STDOUTERR in a_comparar:
449             a_comparar.remove(self.STDOUTERR)
450             diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
451                 zip_a_comparar, zip, self.STDOUTERR,
452                 _(u'La salida estándar y de error combinada'))
453         else:
454             if self.STDOUT in a_comparar:
455                 a_comparar.remove(self.STDOUT)
456                 diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
457                     zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
458             if self.STDERR in a_comparar:
459                 a_comparar.remove(self.STDERR)
460                 diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
461                     zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
462         # Comparamos otros
463         for f in a_comparar:
464             if not os.path.exists(join(path, f)):
465                 if self.rechazar_si_falla:
466                     entrega.exito = False
467                 comando_ejecutado.exito = False
468                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
469                     u'"%s" para comparar pero no fue encontrado') % f
470                 log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
471                     u'no fue encontrado'), f)
472             else:
473                 diff(join(path, f), zip_a_comparar, zip, f)
474         zip.close()
475         comando_ejecutado.diferencias = buffer.getvalue()
476     if comando_ejecutado.exito is None:
477         comando_ejecutado.exito = True
478     elif self.terminar_si_falla:
479         raise ExecutionFailure(self)
480
481 ComandoFuente.ejecutar = ejecutar_comando_fuente
482 #}}}
483
484 def ejecutar_comando_prueba(self, path, prueba): #{{{
485     # Diferencia con comando fuente: s/entrega/prueba/ y s/build/test/ en path
486     # y setup/clean de test.
487     log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
488         prueba.shortrepr())
489     comando_ejecutado = prueba.add_comando_ejecutado(self) # TODO debería rodear solo la ejecución del comando
490     basetmp = '/tmp/sercom.tester.prueba' # FIXME TODO /var/run/sercom?
491     #{{{ Código que solo va en ComandoPrueba (setup de directorio)
492     rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
493         '--archive', '--acls', '--delete-during', '--force', # TODO config
494         'var/chroot_pepe/home/sercom/build/', path) # FIXME!!!! path
495     log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
496     os.seteuid(0) # Dios! (para chroot)
497     os.setegid(0)
498     try:
499         sp.check_call(rsync)
500     finally:
501         os.setegid(user_info.gid) # Mortal de nuevo
502         os.seteuid(user_info.uid)
503         log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
504             user_info.user, user_info.group, user_info.uid, user_info.gid)
505     unzip(prueba.caso_de_prueba.archivos_entrada, path, # TODO try/except
506         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
507     #}}}
508     unzip(self.archivos_entrada, path, # TODO try/except
509         {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
510     options = dict(
511         close_fds=True,
512         shell=True,
513         preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/test') # FIXME!!!! path
514     )
515     if os.path.exists('%s.%s.stdin' % (basetmp, comando_ejecutado.id)):
516         options['stdin'] = file('%s.%s.stdin' % (basetmp, comando_ejecutado.id),
517             'r')
518     else:
519         options['preexec_fn'].close_stdin = True
520     a_guardar = set(self.archivos_a_guardar)
521     if self.archivos_a_comparar:
522         zip_a_comparar = ZipFile(StringIO(self.archivos_a_comparar), 'r')
523         a_comparar = set(zip_a_comparar.namelist())
524     else:
525         zip_a_comparar = None
526         a_comparar = frozenset()
527     a_usar = frozenset(a_guardar | a_comparar)
528     if self.STDOUTERR in a_usar:
529         options['stdout'] = file('%s.%s.stdouterr' % (basetmp,
530             comando_ejecutado.id), 'w')
531         options['stderr'] = sp.STDOUT
532     else:
533         if self.STDOUT in a_usar:
534             options['stdout'] = file('%s.%s.stdout' % (basetmp,
535                 comando_ejecutado.id), 'w')
536         else:
537             options['preexec_fn'].close_stdout = True
538         if self.STDERR in a_usar:
539             options['stderr'] = file('%s.%s.stderr' % (basetmp,
540                 comando_ejecutado.id), 'w')
541         else:
542             options['preexec_fn'].close_stderr = True
543     log.debug(_(u'Ejecutando como root: %s'), self.comando)
544     os.seteuid(0) # Dios! (para chroot)
545     os.setegid(0)
546     try:
547         try:
548             proc = sp.Popen(self.comando, **options)
549         finally:
550             os.setegid(user_info.gid) # Mortal de nuevo
551             os.seteuid(user_info.uid)
552             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
553                 user_info.user, user_info.group, user_info.uid, user_info.gid)
554     except Exception, e:
555         if hasattr(e, 'child_traceback'):
556             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
557         raise
558     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
559     comando_ejecutado.fin_tareas = datetime.now() # TODO debería rodear solo la ejecución del comando
560     if self.retorno != self.RET_ANY:
561         if self.retorno == self.RET_FAIL:
562             if proc.returncode == 0:
563                 if self.rechazar_si_falla:
564                     prueba.exito = False
565                 comando_ejecutado.exito = False
566                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
567                     u'programa termine con un error (código de retorno '
568                     u'distinto de 0) pero terminó bien (código de retorno '
569                     u'0).\n')
570                 log.debug(_(u'Se esperaba que el programa termine '
571                     u'con un error (código de retorno distinto de 0) pero '
572                     u'terminó bien (código de retorno 0).\n'))
573         elif self.retorno != proc.returncode:
574             if self.rechazar_si_falla:
575                 prueba.exito = False
576             comando_ejecutado.exito = False
577             if proc.returncode < 0:
578                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
579                     u'con un código de retorno %s pero se obtuvo una señal %s '
580                     u'(%s).\n') % (self.retorno, -proc.returncode,
581                         -proc.returncode) # TODO poner con texto
582                 log.debug(_(u'Se esperaba terminar con un código '
583                     u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
584                     self.retorno, -proc.returncode, -proc.returncode)
585             else:
586                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
587                     u'con un código de retorno %s pero se obtuvo %s.\n') \
588                     % (self.retorno, proc.returncode)
589                 log.debug(_(u'Se esperaba terminar con un código de retorno '
590                     u'%s pero se obtuvo %s.\n'), self.retorno, proc.returncode)
591     if comando_ejecutado.exito is None:
592         log.debug(_(u'Código de retorno OK'))
593     if a_guardar:
594         buffer = StringIO()
595         zip = ZipFile(buffer, 'w')
596         # Guardamos stdout/stderr
597         if self.STDOUTERR in a_guardar:
598             a_guardar.remove(self.STDOUTERR)
599             zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
600                 self.STDOUTERR)
601         else:
602             if self.STDOUT in a_guardar:
603                 a_guardar.remove(self.STDOUT)
604                 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
605                     self.STDOUT)
606             if self.STDERR in a_guardar:
607                 a_guardar.remove(self.STDERR)
608                 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
609                     self.STDERR)
610         # Guardamos otros
611         for f in a_guardar:
612             if not os.path.exists(join(path, f)):
613                 if self.rechazar_si_falla:
614                     prueba.exito = False
615                 comando_ejecutado.exito = False
616                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
617                     u'"%s" para guardar pero no fue encontrado.\n') % f
618                 log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
619                     u'no fue encontrado'), f)
620             else:
621                 zip.write(join(path, f), f)
622         zip.close()
623         comando_ejecutado.archivos = buffer.getvalue()
624     def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
625              newname='entregado'):
626         if longname is None:
627             longname = name
628         new = file(new, 'r').readlines()
629         orig = zip_in.read(name).split('\n')
630         udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
631             tofile=name+'.'+newname)))
632         if udiff:
633             if self.rechazar_si_falla:
634                 prueba.exito = False
635             comando_ejecutado.exito = False
636             comando_ejecutado.observaciones += _(u'%s no coincide con lo '
637                 u'esperado (archivo "%s.diff").\n') % (longname, name)
638             log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
639                 longname, name)
640             htmldiff = HtmlDiff().make_file(orig, new,
641                 fromdesc=name+'.'+origname, todesc=name+'.'+newname,
642                 context=True, numlines=3)
643             zip_out.writestr(name + '.diff', udiff)
644             zip_out.writestr(name + '.diff.html', htmldiff)
645             return True
646         else:
647             return False
648     if a_comparar:
649         buffer = StringIO()
650         zip = ZipFile(buffer, 'w')
651         # Comparamos stdout/stderr
652         if self.STDOUTERR in a_comparar:
653             a_comparar.remove(self.STDOUTERR)
654             diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
655                 zip_a_comparar, zip, self.STDOUTERR,
656                 _(u'La salida estándar y de error combinada'))
657         else:
658             if self.STDOUT in a_comparar:
659                 a_comparar.remove(self.STDOUT)
660                 diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
661                     zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
662             if self.STDERR in a_comparar:
663                 a_comparar.remove(self.STDERR)
664                 diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
665                     zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
666         # Comparamos otros
667         for f in a_comparar:
668             if not os.path.exists(join(path, f)):
669                 if self.rechazar_si_falla:
670                     prueba.exito = False
671                 comando_ejecutado.exito = False
672                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
673                     u'"%s" para comparar pero no fue encontrado') % f
674                 log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
675                     u'no fue encontrado'), f)
676             else:
677                 diff(join(path, f), zip_a_comparar, zip, f)
678         zip.close()
679         comando_ejecutado.diferencias = buffer.getvalue()
680     if comando_ejecutado.exito is None:
681         comando_ejecutado.exito = True
682     elif self.terminar_si_falla:
683         raise ExecutionFailure(self)
684
685 ComandoPrueba.ejecutar = ejecutar_comando_prueba
686 #}}}
687