]> git.llucax.com Git - z.facultad/75.52/sercom.git/blob - sercom/tester.py
Arreglar y completar ExecutionFailure.
[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
4 from sercom.model import TareaFuente, TareaPrueba, ComandoFuente, ComandoPrueba
5 from zipfile import ZipFile, BadZipfile
6 from cStringIO import StringIO
7 from shutil import rmtree
8 from datetime import datetime
9 from os.path import join
10 from turbogears import config
11 import subprocess as sp
12 import os, sys, pwd, grp
13 import resource as rsrc
14 import logging
15
16 log = logging.getLogger('sercom.tester')
17
18 error_interno = _(u'\n**Error interno al preparar la entrega.**')
19
20 class UserInfo(object): #{{{
21     def __init__(self, user):
22         try:
23             info = pwd.getpwnam(user)
24         except:
25             info = pwd.get(int(user))
26         self.user = info[0]
27         self.uid = info[2]
28         self.gid = info[3]
29         self.name = info[4]
30         self.home = info[5]
31         self.shell = info[6]
32         self.group = grp.getgrgid(self.gid)[0]
33 #}}}
34
35 user_info = UserInfo(config.get('sercom.tester.user', 65534))
36
37 def check_call(*popenargs, **kwargs): #{{{ XXX Python 2.5 forward-compatibility
38     """Run command with arguments.  Wait for command to complete.  If
39     the exit code was zero then return, otherwise raise
40     CalledProcessError.  The CalledProcessError object will have the
41     return code in the returncode attribute.
42     ret = call(*popenargs, **kwargs)
43
44     The arguments are the same as for the Popen constructor.  Example:
45
46     check_call(["ls", "-l"])
47     """
48     retcode = sp.call(*popenargs, **kwargs)
49     cmd = kwargs.get("args")
50     if cmd is None:
51         cmd = popenargs[0]
52     if retcode:
53         raise sp.CalledProcessError(retcode, cmd)
54     return retcode
55 sp.check_call = check_call
56 #}}}
57
58 #{{{ Excepciones
59
60 class CalledProcessError(Exception): #{{{ XXX Python 2.5 forward-compatibility
61     """This exception is raised when a process run by check_call() returns
62     a non-zero exit status.  The exit status will be stored in the
63     returncode attribute."""
64     def __init__(self, returncode, cmd):
65         self.returncode = returncode
66         self.cmd = cmd
67     def __str__(self):
68         return ("Command '%s' returned non-zero exit status %d"
69             % (self.cmd, self.returncode))
70 sp.CalledProcessError = CalledProcessError
71 #}}}
72
73 class Error(StandardError): pass
74
75 class ExecutionFailure(Error, RuntimeError): #{{{
76     def __init__(self, comando, tarea=None, caso_de_prueba=None):
77         self.comando = comando
78         self.tarea = tarea
79         self.caso_de_prueba = caso_de_prueba
80 #}}}
81
82 class RsyncError(Error, EnvironmentError): pass
83
84 #}}}
85
86 def unzip(bytes, default_dst='.', specific_dst=dict()): # {{{
87     u"""Descomprime un buffer de datos en formato ZIP.
88     Los archivos se descomprimen en default_dst a menos que exista una entrada
89     en specific_dst cuya clave sea el nombre de archivo a descomprimir, en
90     cuyo caso, se descomprime usando como destino el valor de dicha clave.
91     """
92     log.debug(_(u'Intentando descomprimir'))
93     if bytes is None:
94         return
95     zfile = ZipFile(StringIO(bytes), 'r')
96     for f in zfile.namelist():
97         dst = join(specific_dst.get(f, default_dst), f)
98         if f.endswith(os.sep):
99             log.debug(_(u'Creando directorio "%s" en "%s"'), f, dst)
100             os.mkdir(dst)
101         else:
102             log.debug(_(u'Descomprimiendo archivo "%s" en "%s"'), f, dst)
103             file(dst, 'w').write(zfile.read(f))
104     zfile.close()
105 #}}}
106
107 class SecureProcess(object): #{{{
108     default = dict(
109         max_tiempo_cpu      = 120,
110         max_memoria         = 16,
111         max_tam_archivo     = 5,
112         max_cant_archivos   = 5,
113         max_cant_procesos   = 0,
114         max_locks_memoria   = 0,
115     )
116     uid = config.get('sercom.tester.chroot.user', 65534)
117     MB = 1048576
118     # XXX probar! make de un solo archivo lleva nproc=100 y nofile=15
119     def __init__(self, comando, chroot, cwd, close_stdin=False,
120                  close_stdout=False, close_stderr=False):
121         self.comando = comando
122         self.chroot = chroot
123         self.cwd = cwd
124         self.close_stdin = close_stdin
125         self.close_stdout = close_stdout
126         self.close_stderr = close_stderr
127         log.debug(_(u'Proceso segurizado: chroot=%s, cwd=%s, user=%s, cpu=%s, '
128             u'as=%sMiB, fsize=%sMiB, nofile=%s, nproc=%s, memlock=%s'),
129             self.chroot, self.cwd, self.uid, self.max_tiempo_cpu,
130             self.max_memoria, self.max_tam_archivo, self.max_cant_archivos,
131             self.max_cant_procesos, self.max_locks_memoria)
132     def __getattr__(self, name):
133         if getattr(self.comando, name) is not None:
134             return getattr(self.comando, name)
135         return config.get('sercom.tester.limits.' + name, self.default[name])
136     def __call__(self):
137         x2 = lambda x: (x, x)
138         if self.close_stdin:
139             os.close(0)
140         if self.close_stdout:
141             os.close(1)
142         if self.close_stderr:
143             os.close(2)
144         os.chroot(self.chroot)
145         os.chdir(self.cwd)
146         uinfo = UserInfo(self.uid)
147         os.setgid(uinfo.gid)
148         os.setuid(uinfo.uid) # Somos mortales irreversiblemente
149         rsrc.setrlimit(rsrc.RLIMIT_CPU, x2(self.max_tiempo_cpu))
150         rsrc.setrlimit(rsrc.RLIMIT_AS, x2(self.max_memoria*self.MB))
151         rsrc.setrlimit(rsrc.RLIMIT_FSIZE, x2(self.max_tam_archivo*self.MB)) # XXX calcular en base a archivos esperados?
152         rsrc.setrlimit(rsrc.RLIMIT_NOFILE, x2(self.max_cant_archivos)) #XXX Obtener de archivos esperados?
153         rsrc.setrlimit(rsrc.RLIMIT_NPROC, x2(self.max_cant_procesos))
154         rsrc.setrlimit(rsrc.RLIMIT_MEMLOCK, x2(self.max_locks_memoria))
155         rsrc.setrlimit(rsrc.RLIMIT_CORE, x2(0))
156         # Tratamos de forzar un sync para que entre al sleep del padre FIXME
157         import time
158         time.sleep(0)
159 #}}}
160
161 class Tester(object): #{{{
162
163     def __init__(self, name, path, home, queue): #{{{ y properties
164         self.name = name
165         self.path = path
166         self.home = home
167         self.queue = queue
168         # Ahora somos mortales (oid mortales)
169         log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
170             user_info.user, user_info.group, user_info.uid, user_info.gid)
171         os.setegid(user_info.gid)
172         os.seteuid(user_info.uid)
173
174     @property
175     def build_path(self):
176         return join(self.chroot, self.home, 'build')
177
178     @property
179     def test_path(self):
180         return join(self.chroot, self.home, 'test')
181
182     @property
183     def chroot(self):
184         return join(self.path, 'chroot_' + self.name)
185
186     @property
187     def orig_chroot(self):
188         return join(self.path, 'chroot')
189     #}}}
190
191     def run(self): #{{{
192         entrega_id = self.queue.get() # blocking
193         while entrega_id is not None:
194             entrega = Entrega.get(entrega_id)
195             log.debug(_(u'Nueva entrega para probar en tester %s: %s'),
196                 self.name, entrega)
197             self.test(entrega)
198             log.debug(_(u'Fin de pruebas de: %s'), entrega)
199             entrega_id = self.queue.get() # blocking
200     #}}}
201
202     def test(self, entrega): #{{{
203         log.debug(_(u'Tester.test(entrega=%s)'), entrega)
204         entrega.inicio_tareas = datetime.now()
205         try:
206             try:
207                 self.setup_chroot(entrega)
208                 self.ejecutar_tareas_fuente(entrega)
209                 self.ejecutar_tareas_prueba(entrega)
210                 self.clean_chroot(entrega)
211             except ExecutionFailure, e:
212                 entrega.correcta = False
213                 log.info(_(u'Entrega incorrecta: %s'), entrega)
214             except Exception, e:
215                 if isinstance(e, SystemExit): raise
216                 entrega.observaciones += error_interno
217                 log.exception(_('Hubo una excepcion inesperada'))
218             except:
219                 entrega.observaciones += error_interno
220                 log.exception(_('Hubo una excepcion inesperada desconocida'))
221             else:
222                 entrega.correcta = True
223                 log.debug(_(u'Entrega correcta: %s'), entrega)
224         finally:
225             entrega.fin_tareas = datetime.now()
226     #}}}
227
228     def setup_chroot(self, entrega): #{{{ y clean_chroot()
229         log.debug(_(u'Tester.setup_chroot(entrega=%s)'), entrega.shortrepr())
230         rsync = ('rsync', '--stats', '--itemize-changes', '--human-readable',
231             '--archive', '--acls', '--delete-during', '--force', # TODO config
232             join(self.orig_chroot, ''), self.chroot)
233         log.debug(_(u'Ejecutando como root: %s'), ' '.join(rsync))
234         os.seteuid(0) # Dios! (para chroot)
235         os.setegid(0)
236         try:
237             sp.check_call(rsync)
238         finally:
239             log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
240                 user_info.user, user_info.group, user_info.uid, user_info.gid)
241             os.setegid(user_info.gid) # Mortal de nuevo
242             os.seteuid(user_info.uid)
243         unzip(entrega.archivos, self.build_path)
244
245     def clean_chroot(self, entrega):
246         log.debug(_(u'Tester.clean_chroot(entrega=%s)'), entrega.shortrepr())
247         pass # Se limpia con el próximo rsync
248     #}}}
249
250     def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
251         log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
252             entrega.shortrepr())
253         tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
254                     if isinstance(t, TareaFuente)]
255         for tarea in tareas:
256             tarea.ejecutar(self.build_path, entrega)
257
258     def ejecutar_tareas_prueba(self, entrega):
259         log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
260             entrega.shortrepr())
261         for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
262             caso.ejecutar(self.test_path, entrega)
263     #}}}
264
265 #}}}
266
267 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
268     log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
269         entrega.shortrepr())
270     tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
271                 if isinstance(t, TareaPrueba)]
272     prueba = entrega.add_prueba(self)
273     try:
274         try:
275             for tarea in tareas:
276                 tarea.ejecutar(path, prueba)
277         except ExecutionFailure, e:
278             prueba.exito = False
279             if self.rechazar_si_falla:
280                 entrega.exito = False
281             if self.terminar_si_falla:
282                 raise ExecutionFailure(e.comando, e.tarea, self)
283         else:
284             prueba.exito = True
285     finally:
286         prueba.fin = datetime.now()
287 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
288 #}}}
289
290 def ejecutar_tarea_fuente(self, path, entrega): #{{{
291     log.debug(_(u'TareaFuente.ejecutar(path=%s, entrega=%s)'), path,
292         entrega.shortrepr())
293     try:
294         for cmd in self.comandos:
295             cmd.ejecutar(path, entrega)
296     except ExecutionFailure, e:
297         if self.rechazar_si_falla:
298             entrega.exito = False
299         if self.terminar_si_falla:
300             raise ExecutionFailure(e.comando, self)
301 TareaFuente.ejecutar = ejecutar_tarea_fuente
302 #}}}
303
304 def ejecutar_tarea_prueba(self, path, prueba): #{{{
305     log.debug(_(u'TareaPrueba.ejecutar(path=%s, prueba=%s)'), path,
306         prueba.shortrepr())
307     try:
308         for cmd in self.comandos:
309             cmd.ejecutar(path, prueba)
310     except ExecutionFailure, e:
311         if self.rechazar_si_falla:
312             prueba.exito = False
313         if self.terminar_si_falla:
314             raise ExecutionFailure(e.comando, self)
315 TareaPrueba.ejecutar = ejecutar_tarea_prueba
316 #}}}
317
318 def ejecutar_comando_fuente(self, path, entrega): #{{{
319     log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
320         entrega.shortrepr())
321     comando_ejecutado = entrega.add_comando_ejecutado(self)
322     unzip(self.archivos_entrada, path, # TODO try/except
323         dict(__stdin__='/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id)) # TODO /var/run/sercom
324     options = dict(
325         close_fds=True,
326         shell=True,
327         preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build')
328     )
329     if os.path.exists('/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id): # TODO
330         options['stdin'] = file('/tmp/sercom.tester.%s.stdin' % comando_ejecutado.id, 'r') # TODO
331     else:
332         options['preexec_fn'].close_stdin = True
333     if self.guardar_stdouterr:
334         options['stdout'] = file('/tmp/sercom.tester.%s.stdouterr'
335             % comando_ejecutado.id, 'w') #TODO /var/run/sercom?
336         options['stderr'] = sp.STDOUT
337     else:
338         if self.guardar_stdout:
339             options['stdout'] = file('/tmp/sercom.tester.%s.stdout'
340                 % comando_ejecutado.id, 'w') #TODO /run/lib/sercom?
341         else:
342             options['preexec_fn'].close_stdout = True
343         if self.guardar_stderr:
344             options['stderr'] = file('/tmp/sercom.tester.%s.stderr'
345                 % comando_ejecutado.id, 'w') #TODO /var/run/sercom?
346         else:
347             options['preexec_fn'].close_stderr = True
348     log.debug(_(u'Ejecutando como root: %s'), self.comando)
349     os.seteuid(0) # Dios! (para chroot)
350     os.setegid(0)
351     try:
352         try:
353             proc = sp.Popen(self.comando, **options)
354         finally:
355             log.debug(_(u'Cambiando usuario y grupo efectivos a %s:%s (%s:%s)'),
356                 user_info.user, user_info.group, user_info.uid, user_info.gid)
357             os.setegid(user_info.gid) # Mortal de nuevo
358             os.seteuid(user_info.uid)
359     except Exception, e:
360         if hasattr(e, 'child_traceback'):
361             log.error(_(u'Error en el hijo: %s'), e.child_traceback)
362         raise
363     proc.wait() #TODO un sleep grande nos caga todo, ver sercom viejo
364     if self.retorno != self.RET_ANY:
365         if self.retorno == self.RET_FAIL:
366             if proc.returncode == 0:
367                 comando_ejecutado.exito = False
368                 comando_ejecutado.observaciones += _(u'Se esperaba que el '
369                     u'programa termine con un error (código de retorno '
370                     u'distinto de 0) pero terminó bien (código de retorno '
371                     u'0).\n')
372                 log.debug(_(u'Se esperaba que el programa termine '
373                     u'con un error (código de retorno distinto de 0) pero '
374                     u'terminó bien (código de retorno 0).\n'))
375         elif self.retorno != proc.returncode:
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') % (self.retorno, -proc.returncode,
381                         -proc.returncode) # TODO poner con texto
382                 log.debug(_(u'Se esperaba terminar con un código '
383                     u'de retorno %s pero se obtuvo una señal %s (%s).\n'),
384                     self.retorno, -proc.returncode, -proc.returncode)
385             else:
386                 comando_ejecutado.observaciones += _(u'Se esperaba terminar '
387                     u'con un código de retorno %s pero se obtuvo %s.\n') \
388                     % (self.retorno, proc.returncode)
389                 log.debug(_(u'Se esperaba terminar con un código de retorno '
390                     u'%s pero se obtuvo %s.\n'), self.retorno, proc.returncode)
391     if comando_ejecutado.exito is None:
392         log.debug(_(u'Código de retorno OK'))
393     comando_ejecutado.fin = datetime.now()
394     buffer = StringIO()
395     zip = ZipFile(buffer, 'w')
396     # Guardamos stdout/stderr
397     if self.guardar_stdouterr:
398         zip.write('/tmp/sercom.tester.%s.stdouterr'
399             % comando_ejecutado.id, '__stdouterr__')
400     else:
401         if self.guardar_stdout:
402             zip.write('/tmp/sercom.tester.%s.stdout'
403                 % comando_ejecutado.id, '__stdout__')
404         if self.guardar_stderr:
405             zip.write('/tmp/sercom.tester.%s.stderr'
406                 % comando_ejecutado.id, '__stderr__')
407     # Guardamos otros
408     for f in self.archivos_a_guardar:
409         if not os.path.exists(join(path, f)):
410             comando_ejecutado.exito = False
411             comando_ejecutado.observaciones += _(u'Se esperaba un archivo "%s" pero no fue '
412                 u'encontrado') % f
413             log.debug(_(u'Se esperaba un archivo "%s" pero no fue '
414                 u'encontrado'), f)
415         else:
416             zip.write(join(path, f), f)
417     zip.close()
418     comando_ejecutado.archivos_guardados = buffer.getvalue()
419
420 #    if no_anda_ejecucion: # TODO
421 #        comando_ejecutado.exito = False
422 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
423 #        if self.rechazar_si_falla:
424 #            entrega.exito = False
425 #        if self.terminar_si_falla: # TODO
426 #            raise ExecutionFailure(self)
427     # XXX ESTO EN REALIDAD EN COMANDOS FUENTE NO IRIA
428     # XXX SOLO HABRÍA QUE CAPTURAR stdout/stderr
429     # XXX PODRIA TENER ARCHIVOS DE SALIDA PERO SOLO PARA MOSTRAR COMO RESULTADO
430 #    for archivo in self.archivos_salida:
431 #        pass # TODO hacer diff
432 #    if archivos_mal: # TODO
433 #        comando_ejecutado.exito = False
434 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO mas info
435 #        if self.rechazar_si_falla:
436 #            entrega.exito = False
437 #        if self.terminar_si_falla: # TODO
438 #            raise ExecutionFailure(self)
439 #    else:
440 #        comando_ejecutado.exito = True
441 #        comando_ejecutado.observaciones += 'xxx OK' # TODO
442     comando_ejecutado.exito = True
443     comando_ejecutado.observaciones += 'xxx OK' # TODO
444 ComandoFuente.ejecutar = ejecutar_comando_fuente
445 #}}}
446
447 def ejecutar_comando_prueba(self, path, prueba): #{{{
448     log.debug(_(u'ComandoPrueba.ejecutar(path=%s, prueba=%s)'), path,
449         prueba.shortrepr())
450     rmtree(path)
451     os.mkdir(path)
452     unzip(prueba.caso_de_prueba.archivos_entrada, path) # TODO try/except
453     unzip(self.archivos_entrada, path) # TODO try/except
454     comando_ejecutado = prueba.add_comando_ejecutado(self)
455     # TODO ejecutar en chroot (path)
456     comando_ejecutado.fin = datetime.now()
457 #    if no_anda_ejecucion: # TODO
458 #        comando_ejecutado.exito = False
459 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
460 #        if self.rechazar_si_falla:
461 #            entrega.exito = False
462 #        if self.terminar_si_falla: # TODO
463 #            raise ExecutionFailure(self) # TODO info de error
464 #    for archivo in self.archivos_salida:
465 #        pass # TODO hacer diff
466 #    if archivos_mal: # TODO
467 #        comando_ejecutado.exito = False
468 #        comando_ejecutado.observaciones += 'No anduvo xxx' # TODO
469 #        if self.rechazar_si_falla:
470 #            entrega.exito = False
471 #        if self.terminar_si_falla: # TODO
472 #            raise ExecutionFailure(comando=self) # TODO info de error
473 #    else:
474 #        comando_ejecutado.exito = True
475 #        comando_ejecutado.observaciones += 'xxx OK' # TODO
476     comando_ejecutado.exito = True
477     comando_ejecutado.observaciones += 'xxx OK' # TODO
478 ComandoPrueba.ejecutar = ejecutar_comando_prueba
479 #}}}
480