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