]> git.llucax.com Git - z.facultad/75.52/sercom.git/blob - sercom/tester.py
Muevo alumno_inscripto dentro de curso
[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     a_guardar |= set(prueba.caso_de_prueba.archivos_a_guardar) # FIXME Esto es propio de ComandoPrueba
525     if self.archivos_a_comparar:
526         zip_a_comparar = ZipFile(StringIO(self.archivos_a_comparar), 'r')
527         a_comparar = set(zip_a_comparar.namelist())
528     else:
529         zip_a_comparar = None
530         a_comparar = frozenset()
531     a_usar = frozenset(a_guardar | a_comparar)
532     if self.STDOUTERR in a_usar:
533         options['stdout'] = file('%s.%s.stdouterr' % (basetmp,
534             comando_ejecutado.id), 'w')
535         options['stderr'] = sp.STDOUT
536     else:
537         if self.STDOUT in a_usar:
538             options['stdout'] = file('%s.%s.stdout' % (basetmp,
539                 comando_ejecutado.id), 'w')
540         else:
541             options['preexec_fn'].close_stdout = True
542         if self.STDERR in a_usar:
543             options['stderr'] = file('%s.%s.stderr' % (basetmp,
544                 comando_ejecutado.id), 'w')
545         else:
546             options['preexec_fn'].close_stderr = True
547     comando = self.comando + ' ' + prueba.caso_de_prueba.comando # FIXME Esto es propio de ComandoPrueba
548     log.debug(_(u'Ejecutando como root: %s'), comando)
549     os.seteuid(0) # Dios! (para chroot)
550     os.setegid(0)
551     try:
552         try:
553             proc = sp.Popen(comando, **options)
554         finally:
555             os.setegid(user_info.gid) # Mortal de nuevo
556             os.seteuid(user_info.uid)
557             log.debug(_(u'Usuario y grupo efectivos cambiados a %s:%s (%s:%s)'),
558                 user_info.user, user_info.group, user_info.uid, user_info.gid)
559     except Exception, e:
560         if hasattr(e, 'child_traceback'):
561             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
562         raise
563     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
564     comando_ejecutado.fin_tareas = datetime.now() # TODO debería rodear solo la ejecución del comando
565     retorno = self.retorno
566     if retorno == self.RET_PRUEBA:                # FIXME Esto es propio de ComandoPrueba
567         retorno = prueba.caso_de_prueba.retorno   # FIXME Esto es propio de ComandoPrueba
568     if retorno != self.RET_ANY:
569         if retorno == self.RET_FAIL:
570             if proc.returncode == 0:
571                 if self.rechazar_si_falla:
572                     prueba.exito = False
573                 comando_ejecutado.exito = False
574                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
575                     u'programa termine con un error (código de retorno '
576                     u'distinto de 0) pero terminó bien (código de retorno '
577                     u'0).\n')
578                 log.debug(_(u'Se esperaba que el programa termine '
579                     u'con un error (código de retorno distinto de 0) pero '
580                     u'terminó bien (código de retorno 0).\n'))
581         elif retorno != proc.returncode:
582             if self.rechazar_si_falla:
583                 prueba.exito = False
584             comando_ejecutado.exito = False
585             if proc.returncode < 0:
586                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
587                     u'con un código de retorno %s pero se obtuvo una señal %s '
588                     u'(%s).\n') % (retorno, -proc.returncode, -proc.returncode) # TODO poner con texto
589                 log.debug(_(u'Se esperaba terminar con un código '
590                     u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
591                     retorno, -proc.returncode, -proc.returncode)
592             else:
593                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
594                     u'con un código de retorno %s pero se obtuvo %s.\n') \
595                     % (retorno, proc.returncode)
596                 log.debug(_(u'Se esperaba terminar con un código de retorno '
597                     u'%s pero se obtuvo %s.\n'), retorno, proc.returncode)
598     if comando_ejecutado.exito is None:
599         log.debug(_(u'Código de retorno OK'))
600     if a_guardar:
601         buffer = StringIO()
602         zip = ZipFile(buffer, 'w')
603         # Guardamos stdout/stderr
604         if self.STDOUTERR in a_guardar:
605             a_guardar.remove(self.STDOUTERR)
606             zip.write('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
607                 self.STDOUTERR)
608         else:
609             if self.STDOUT in a_guardar:
610                 a_guardar.remove(self.STDOUT)
611                 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
612                     self.STDOUT)
613             if self.STDERR in a_guardar:
614                 a_guardar.remove(self.STDERR)
615                 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
616                     self.STDERR)
617         # Guardamos otros
618         for f in a_guardar:
619             if not os.path.exists(join(path, f)):
620                 if self.rechazar_si_falla:
621                     prueba.exito = False
622                 comando_ejecutado.exito = False
623                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
624                     u'"%s" para guardar pero no fue encontrado.\n') % f
625                 log.debug(_(u'Se esperaba un archivo "%s" para guardar pero '
626                     u'no fue encontrado'), f)
627             else:
628                 zip.write(join(path, f), f)
629         zip.close()
630         comando_ejecutado.archivos = buffer.getvalue()
631     def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
632              newname='entregado'):
633         if longname is None:
634             longname = name
635         new = file(new, 'r').readlines()
636         orig = zip_in.read(name).split('\n')
637         udiff = ''.join(list(unified_diff(orig, new, fromfile=name+'.'+origname,
638             tofile=name+'.'+newname)))
639         if udiff:
640             if self.rechazar_si_falla:
641                 prueba.exito = False
642             comando_ejecutado.exito = False
643             comando_ejecutado.observaciones += _(u'%s no coincide con lo '
644                 u'esperado (archivo "%s.diff").\n') % (longname, name)
645             log.debug(_(u'%s no coincide con lo esperado (archivo "%s.diff")'),
646                 longname, name)
647             htmldiff = HtmlDiff().make_file(orig, new,
648                 fromdesc=name+'.'+origname, todesc=name+'.'+newname,
649                 context=True, numlines=3)
650             zip_out.writestr(name + '.diff', udiff)
651             zip_out.writestr(name + '.diff.html', htmldiff)
652             return True
653         else:
654             return False
655     if a_comparar:
656         buffer = StringIO()
657         zip = ZipFile(buffer, 'w')
658         # Comparamos stdout/stderr
659         if self.STDOUTERR in a_comparar:
660             a_comparar.remove(self.STDOUTERR)
661             diff('%s.%s.stdouterr' % (basetmp, comando_ejecutado.id),
662                 zip_a_comparar, zip, self.STDOUTERR,
663                 _(u'La salida estándar y de error combinada'))
664         else:
665             if self.STDOUT in a_comparar:
666                 a_comparar.remove(self.STDOUT)
667                 diff('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
668                     zip_a_comparar, zip, self.STDOUT, _(u'La salida estándar'))
669             if self.STDERR in a_comparar:
670                 a_comparar.remove(self.STDERR)
671                 diff('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
672                     zip_a_comparar, zip, self.STDERR, _(u'La salida de error'))
673         # Comparamos otros
674         for f in a_comparar:
675             if not os.path.exists(join(path, f)):
676                 if self.rechazar_si_falla:
677                     prueba.exito = False
678                 comando_ejecutado.exito = False
679                 comando_ejecutado.observaciones += _(u'Se esperaba un archivo '
680                     u'"%s" para comparar pero no fue encontrado') % f
681                 log.debug(_(u'Se esperaba un archivo "%s" para comparar pero '
682                     u'no fue encontrado'), f)
683             else:
684                 diff(join(path, f), zip_a_comparar, zip, f)
685         zip.close()
686         comando_ejecutado.diferencias = buffer.getvalue()
687     if comando_ejecutado.exito is None:
688         comando_ejecutado.exito = True
689     elif self.terminar_si_falla:
690         raise ExecutionFailure(self)
691
692 ComandoPrueba.ejecutar = ejecutar_comando_prueba
693 #}}}
694