[pyar] os.fork()+sys.exit() en UnitTests
Marcos Dione
mdione en grulic.org.ar
Vie Ene 10 19:38:10 ART 2014
años sin escribir a la lista, qué vergogna...
estoy renegando con nu test en ayrton. una de las funciones centrales
es la que ejecuta un binario bajo cierto environment. esta función hace un
fork(), lo cual ya es un dolor de gónadas con UT. en el child hace un
execvpe(), función que puede fallar si el binario no existe. en ese caso
catcheo la excepción y hago un sys.exit (127), que es lo que hace bash
cuando no encuentra el ejecutable.
la idea es que del lado del parent hago un waitpid(), y si el hijo
termina con un exit code !=0, y se da una condición, raiseo otra excepción.
y es justamente esto último lo que estoy queriendo testear: que cuando
el binario no existe, esa exepción es raiseada en el padre. acá un resumen
del código[1]:
r= os.fork ()
if r==0:
try:
os.execvpe (cmd, args, self.options['_env'])
except OSError:
sys.exit (127)
else:
self.exit_code= os.waitpid (child_pid, 0)[1]
if runner.options.get ('errexit', False) and self.exit_code==127:
raise CommandFailed (self)
y el del test[2]:
def testFromImportAsFails (self):
self.assertRaises (CommandNotFound, ayrton.main,
'''from random import seed as foo
bar ()''')
no es evidente del texto del test, pero esa llamada a bar() trata de
ejecutar un binario llamado bar en algún lado del path. de todas formas,
no es parte del problema. el problema es el siguiente:
por un lado, fork() no se lleva muy bien con unittest, eso es claro. lo
que sucede es que después del fork(), si la ejecución no termina abruptamente,
el test runner del hijo sigue corriendo y así por cada fork tenés el doble de
procesos que antes. lo que se llama un forkbomb, pero limitado a la cantidad de
tests que forkean. sé que lo que me van a decir es que mockee el fork(), pero
justamente quiero probar que esa comunicación entre el hijo y el padre se dá:
el hijo termina con 127, el padre se dá cuenta y reacciona como corresponde.
lo otro que sucede es que la implementación de sys.exit() cambió en algún
momento[3], ahora raisea SystemExit. y unittest es suficientemente gilastrún de
no saber que ese SystemExit está bién, y en cambio lo agarra con
un except catch-all[4] y lo considera un error porque no es la excepción que
espera que salte. una ejecución:
mdione en diablo:~/src/projects/ayrton$ python3 -m unittest discover -f -v ayrton
[...]
testFromImportAsFails (tests.test_ayrton.CommandDetection) ... ERROR
======================================================================
ERROR: testFromImportAsFails (tests.test_ayrton.CommandDetection)
----------------------------------------------------------------------
Traceback (most recent call last):
File "./ayrton/execute.py", line 165, in child
os.execvpe (cmd, args, self.options['_env'])
File "/usr/lib/python3.3/os.py", line 575, in execvpe
_execvpe(file, args, env)
File "/usr/lib/python3.3/os.py", line 611, in _execvpe
raise last_exc.with_traceback(tb)
File "/usr/lib/python3.3/os.py", line 601, in _execvpe
exec_func(fullname, *argrest)
FileNotFoundError: [Errno 2] No such file or directory: '/usr/bin/bar'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/mdione/src/projects/ayrton/ayrton/tests/test_ayrton.py", line 311, in testFromImportAsFails
bar ()''')
File "/usr/lib/python3.3/unittest/case.py", line 570, in assertRaises
return context.handle('assertRaises', callableObj, args, kwargs)
File "/usr/lib/python3.3/unittest/case.py", line 135, in handle
callable_obj(*args, **kwargs)
File "./ayrton/__init__.py", line 172, in main
runner.run ()
File "./ayrton/__init__.py", line 129, in run
exec (self.source, self.environ.globals, self.environ)
File "arg_to_main", line 2, in <module>
File "./ayrton/execute.py", line 285, in __call__
self.child (self.path, *args, **kwargs)
File "./ayrton/execute.py", line 169, in child
sys.exit (127)
SystemExit: 127
----------------------------------------------------------------------
Ran 50 tests in 0.141s
FAILED (errors=1)
FAIL
======================================================================
FAIL: testFromImportAsFails (tests.test_ayrton.CommandDetection)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/mdione/src/projects/ayrton/ayrton/tests/test_ayrton.py", line 311, in testFromImportAsFails
bar ()''')
AssertionError: CommandNotFound not raised by main
----------------------------------------------------------------------
Ran 50 tests in 0.159s
FAILED (failures=1)
notar cómo ahí se ven los dos resultados: el hijo muere primero fallando
con SystemExit y luego el padre con 'CommandNotFound not raised by main'.
bueno, tons la pregunta es: qué puedo hacer? unittest no parece tener nada
para este tipo de situaciones y realmente mockear no creo que me sirva. mal que
mal ya sé que el código anda como quiero, pero me jode no poder testearlo. si
quieren probar el código, el repo de ayrton está en github[5], hagan un checkout
del branch no-sh (me estoy sacando a sh de encima como dependencia).
--
[1] sino pueden ver el código real en estos links:
fork(): https://github.com/StyXman/ayrton/blob/no-sh/ayrton/execute.py#L260
código en el child: https://github.com/StyXman/ayrton/blob/no-sh/ayrton/execute.py#L162
en el parent: https://github.com/StyXman/ayrton/blob/no-sh/ayrton/execute.py#L220
en realidad las últimas dos líneas están descomentadas en el código con el que estoy jugando.
[2] here: https://github.com/StyXman/ayrton/blob/no-sh/ayrton/tests/test_ayrton.py#L308
[3] 404 Not Found, sorry
[4] http://hg.python.org/cpython/file/c3896275c0f6/Lib/unittest/case.py#l408
[5] https://github.com/StyXman/ayrton/
--
(Not so) Random fortune:
Debido a que la velocidad de la luz es varias veces mayor a la del
sonido, ciertas personas pueden parecernos brillantes antes de escuchar
las pelotudeces que dicen.
More information about the pyar
mailing list