nose-devs / nose

nose is nicer testing for python
http://readthedocs.org/docs/nose/en/latest/
1.36k stars 395 forks source link

Socket created in a daemon thread does not start to listen under nose no matter how long it is waited for #599

Closed p closed 11 years ago

p commented 11 years ago

Please see the last comment for a reduced test case.


This is going to be a long report, sorry. You can skip straight to the reproduce code in https://github.com/p/test-repo/tree/master/nose/http-listen or read the background below.

I am writing a library for testing webapps (https://github.com/p/owebunit - similar to cherrypy webtest or twill). The library has a test suite. In order to exercise library code, the library test suite has various test applications which the tests perform requests against. These test applications use bottle (http://bottlepy.org/docs/dev/) for no particular reason other than bottle is a single file framework and does not require boilerplate.

The way test applications are implemented is they are defined and started from global module scope. This is probably suboptimal but it works well enough presently.

Recently I started using nose to run the test suite. What happens in the nose + bottle combination is, whenever I run an individual test file via nose the first request to the test application fails because the app is not listening. I verified that the app is not listening via netstat. Requests past the first one succeed. This happens about 99% of the time. The remaining 1% all tests succeed.

If nosetests is run without arguments to run the full test suite, the test app still does not begin to listen while I am waiting for it but all tests succeed. So far the success rate in this scenario has been 100% unlike the 99% failure rate in running individual tests.

I used to have a simple 0.1 second delay after starting the test app's server before continuing with module init (remember app is launched from global scope). Then I changed this to polling for the port (via a socket connection) to start accepting connections and noticed that no matter how long I waited, the test app never began listening, until I started sending legitimate http requests.

I reduced the test case to https://github.com/p/test-repo/tree/master/nose/http-listen. While writing that I used wsgiref.demo_app directly and noticed that wsgiref worked fine with nose. Using bottle was required to reproduce the behavior I was seeing.

At this point I don't know if the responsible party is nose or bottle. Any thoughts and ideas for getting to the bottom of this are appreciated.

The actual test suite is https://github.com/p/owebunit/tree/master/tests and you can see a warning message that the server did not begin listening at https://travis-ci.org/p/owebunit/jobs/4121967 for example.

p commented 11 years ago

I added a test using itty (https://github.com/toastdriven/itty) rather than bottle. Itty is about 1/4 the size of bottle. Same behavior as bottle with respect to this report.

On python 3 the probability of failure in actual tests when the full suite invoked via nosetests seems to be higher.

p commented 11 years ago

Managed to reduce the test case further.

Performing wsgiref import in the daemon thread makes wsgiref fail with the same behavior as bottle/itty.

Test files:

https://github.com/p/test-repo/blob/master/nose/http-listen/tests/wsgiref_test.py https://github.com/p/test-repo/blob/master/nose/http-listen/tests/wsgiref_broken_test.py

The only difference is location of wsgiref import.

New description: https://github.com/p/test-repo/tree/master/nose/http-listen

p commented 11 years ago

I reduced this now to a socket only test:

https://github.com/p/test-repo/blob/master/nose/http-listen/tests/socket_test.py

and https://github.com/p/test-repo/blob/master/nose/http-listen/tests/fail.py which is slightly smaller but not a usable test (still produces the same failure to listen under nose).

This rather looks like a nose bug.

Also, because the bug manifests even when importing modules that have already been imported, so far I have not been able to come up with a workaround for it.

pitrou commented 11 years ago

You say: "Having an import here prevents the app from listening".

Indeed, this is the Python import lock in action. Please read http://docs.python.org/2/library/threading.html#importing-in-threaded-code and http://docs.python.org/2/library/imp.html#imp.lock_held

You get the exact same behaviour if, instead of typing nosetests, you type python -m unittest socket_test. Therefore, recommend closing as this is not a nose issue.

(note, it should work in Python 3.3 where the import lock is more fine-grained)

p commented 11 years ago

That makes sense. The solution would be to not launch the app from global scope then. Thanks for the doc pointers.

p commented 11 years ago

To follow up on this, I was able to easily achieve the desired behavior the right way via a module level setup function.