1 # vim: set et sw=4 sts=4 encoding=utf-8 foldmethod=marker:
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
17 log = logging.getLogger('sercom.tester')
19 error_interno = _(u'\n**Error interno al preparar la entrega.**')
21 class UserInfo(object): #{{{
22 def __init__(self, user):
24 info = pwd.getpwnam(user)
26 info = pwd.get(int(user))
33 self.group = grp.getgrgid(self.gid)[0]
36 user_info = UserInfo(config.get('sercom.tester.user', 65534))
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)
45 The arguments are the same as for the Popen constructor. Example:
47 check_call(["ls", "-l"])
49 retcode = sp.call(*popenargs, **kwargs)
50 cmd = kwargs.get("args")
54 raise sp.CalledProcessError(retcode, cmd)
56 sp.check_call = check_call
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
69 return ("Command '%s' returned non-zero exit status %d"
70 % (self.cmd, self.returncode))
71 sp.CalledProcessError = CalledProcessError
74 class Error(StandardError): pass
76 class ExecutionFailure(Error, RuntimeError): #{{{
77 def __init__(self, comando, tarea=None, caso_de_prueba=None):
78 self.comando = comando
80 self.caso_de_prueba = caso_de_prueba
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.
91 log.debug(_(u'Intentando descomprimir'))
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)
101 log.debug(_(u'Descomprimiendo archivo "%s" en "%s"'), f, dst)
102 file(dst, 'w').write(zfile.read(f))
106 class SecureProcess(object): #{{{
108 max_tiempo_cpu = 120,
111 max_cant_archivos = 5,
112 max_cant_procesos = 0,
113 max_locks_memoria = 0,
115 uid = config.get('sercom.tester.chroot.user', 65534)
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
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])
136 x2 = lambda x: (x, x)
139 if self.close_stdout:
141 if self.close_stderr:
143 os.chroot(self.chroot)
145 uinfo = UserInfo(self.uid)
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
160 class Tester(object): #{{{
162 def __init__(self, name, path, home, queue): #{{{ y properties
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)
174 def build_path(self):
175 return join(self.chroot, self.home, 'build')
179 return join(self.chroot, self.home, 'test')
183 return join(self.path, 'chroot_' + self.name)
186 def orig_chroot(self):
187 return join(self.path, 'chroot')
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'),
197 log.debug(_(u'Fin de pruebas de: %s'), entrega)
198 entrega_id = self.queue.get() # blocking
201 def test(self, entrega): #{{{
202 log.debug(_(u'Tester.test(entrega=%s)'), entrega)
203 entrega.inicio = datetime.now()
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)
214 if isinstance(e, SystemExit): raise
215 entrega.observaciones += error_interno
216 log.exception(_('Hubo una excepcion inesperada')) # FIXME encoding
218 entrega.observaciones += error_interno
219 log.exception(_('Hubo una excepcion inesperada desconocida')) # FIXME encoding
222 log.debug(_(u'Entrega correcta: %s'), entrega)
224 entrega.fin = datetime.now()
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)
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)
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
249 def ejecutar_tareas_fuente(self, entrega): #{{{ y tareas_prueba
250 log.debug(_(u'Tester.ejecutar_tareas_fuente(entrega=%s)'),
252 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
253 if isinstance(t, TareaFuente)]
255 tarea.ejecutar(self.build_path, entrega)
257 def ejecutar_tareas_prueba(self, entrega):
258 log.debug(_(u'Tester.ejecutar_tareas_prueba(entrega=%s)'),
260 for caso in entrega.instancia.ejercicio.enunciado.casos_de_prueba:
261 caso.ejecutar(self.test_path, entrega)
266 def ejecutar_caso_de_prueba(self, path, entrega): #{{{
267 log.debug(_(u'CasoDePrueba.ejecutar(path=%s, entrega=%s)'), path,
269 tareas = [t for t in entrega.instancia.ejercicio.enunciado.tareas
270 if isinstance(t, TareaPrueba)]
271 prueba = entrega.add_prueba(self)
275 tarea.ejecutar(path, prueba)
276 except ExecutionFailure, e:
278 if self.rechazar_si_falla:
279 entrega.exito = False
280 if self.terminar_si_falla:
281 raise ExecutionFailure(e.comando, e.tarea, self)
285 prueba.fin = datetime.now()
286 CasoDePrueba.ejecutar = ejecutar_caso_de_prueba
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
297 # TODO generalizar ejecutar_comando_xxxx!!!
299 def ejecutar_comando_fuente(self, path, entrega): #{{{
300 log.debug(_(u'ComandoFuente.ejecutar(path=%s, entrega=%s)'), path,
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)})
309 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/build') #FIXME!!! path
311 if os.path.exists('%s.%s.stdin' % (basetmp, comando_ejecutado.id)):
312 options['stdin'] = file('%s.%s.stdin' % (basetmp, comando_ejecutado.id),
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())
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
329 if self.STDOUT in a_usar:
330 options['stdout'] = file('%s.%s.stdout' % (basetmp,
331 comando_ejecutado.id), 'w')
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')
338 options['preexec_fn'].close_stderr = True
339 log.debug(_(u'Ejecutando como root: %s'), self.comando)
340 os.seteuid(0) # Dios! (para chroot)
344 proc = sp.Popen(self.comando, **options)
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)
351 if hasattr(e, 'child_traceback'):
352 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
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 '
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)
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'))
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),
398 if self.STDOUT in a_guardar:
399 a_guardar.remove(self.STDOUT)
400 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
402 if self.STDERR in a_guardar:
403 a_guardar.remove(self.STDERR)
404 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
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)
417 zip.write(join(path, f), f)
419 comando_ejecutado.archivos = buffer.getvalue()
420 def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
421 newname='entregado'):
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)))
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")'),
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)
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'))
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'))
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)
473 diff(join(path, f), zip_a_comparar, zip, f)
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)
481 ComandoFuente.ejecutar = ejecutar_comando_fuente
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,
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)
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)})
508 unzip(self.archivos_entrada, path, # TODO try/except
509 {self.STDIN: '%s.%s.stdin' % (basetmp, comando_ejecutado.id)})
513 preexec_fn=SecureProcess(self, 'var/chroot_pepe', '/home/sercom/test') # FIXME!!!! path
515 if os.path.exists('%s.%s.stdin' % (basetmp, comando_ejecutado.id)):
516 options['stdin'] = file('%s.%s.stdin' % (basetmp, comando_ejecutado.id),
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())
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
533 if self.STDOUT in a_usar:
534 options['stdout'] = file('%s.%s.stdout' % (basetmp,
535 comando_ejecutado.id), 'w')
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')
542 options['preexec_fn'].close_stderr = True
543 log.debug(_(u'Ejecutando como root: %s'), self.comando)
544 os.seteuid(0) # Dios! (para chroot)
548 proc = sp.Popen(self.comando, **options)
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)
555 if hasattr(e, 'child_traceback'):
556 log.error(_(u'Error en el hijo: %s'), e.child_traceback)
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:
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 '
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:
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)
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'))
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),
602 if self.STDOUT in a_guardar:
603 a_guardar.remove(self.STDOUT)
604 zip.write('%s.%s.stdout' % (basetmp, comando_ejecutado.id),
606 if self.STDERR in a_guardar:
607 a_guardar.remove(self.STDERR)
608 zip.write('%s.%s.stderr' % (basetmp, comando_ejecutado.id),
612 if not os.path.exists(join(path, f)):
613 if self.rechazar_si_falla:
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)
621 zip.write(join(path, f), f)
623 comando_ejecutado.archivos = buffer.getvalue()
624 def diff(new, zip_in, zip_out, name, longname=None, origname='correcto',
625 newname='entregado'):
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)))
633 if self.rechazar_si_falla:
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")'),
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)
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'))
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'))
668 if not os.path.exists(join(path, f)):
669 if self.rechazar_si_falla:
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)
677 diff(join(path, f), zip_a_comparar, zip, f)
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)
685 ComandoPrueba.ejecutar = ejecutar_comando_prueba