[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