IndexError on stop()
when originating thread exits before completion · Issue #1542 · nedbat/coveragepy (original) (raw)
Describe the bug
If the thread where Coverage was started has exited, coverage will raise an IndexError when the PyTracer cleans up, because it will try to pop an empty data stack.
We're seeing this with 100% reliability when running Coverage on a test suite on an Android GUI app. While the circumstances on Android is unusual, I think it's theoretically reproducible in any app that has a C mainline, using Python in embedded mode.
To Reproduce
The general workflow leading to this bug is as follows:
- A mainline C program starts, and initializes an embedded CPython interpreter.
- The CPython interpreter starts a Python script
- The Python script initialises and starts coverage using
timid=True
(forcing the use of the PyTracer) - The Python script starts a second thread, that does the activity you want to evaluate coverage on (in our case, a Pytest suite)
- The main Python thread exits. It does not wait for the second thread to complete, or join on the second thread.
- The main C program goes into a "while 1" loop, awaiting for the second thread to finish
- The second thread finishes, stops coverage, outputs a report, and sets a flag that notifies the main C program that it can exit.
The key detail is (5) & (6) - the Python code for the main thread has exited, but the main thread still exists and is running.
During (7), PyTracer._trace()
will be invoked, will identify that the main thread has been stopped, and try to clean up; on line 130 (as of Coverage 7.0.5), the data stack will be popped; however, the data stack will be empty, and an IndexError will be raised.
The presentation of the exception Android is a little unusual, because of the rest of the embedded code needed to make Python code run on Android - but the source of the error is clear:
--------- beginning of crash
01-24 10:32:37.579 15673 15673 E AndroidRuntime: FATAL EXCEPTION: main
01-24 10:32:37.579 15673 15673 E AndroidRuntime: Process: org.beeware.toga.testbed, PID: 15673
01-24 10:32:37.579 15673 15673 E AndroidRuntime: com.chaquo.python.PyException: IndexError: pop from empty list
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at <python>.coverage.pytracer._trace(pytracer.py:130)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at <python>.weakref.get(weakref.py:195)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at <python>.java.chaquopy.JavaClass.__call__(class.pxi:112)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at <python>.java.chaquopy.JavaClass.__call__(class.pxi:111)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at <python>.java.chaquopy.DynamicProxyClass.__call__(proxy.pxi:58)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at <python>.java.chaquopy.j2p(conversion.pxi:94)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at <python>.chaquopy_java.Java_com_chaquo_python_PyObject_fromJavaNative(chaquopy_java.pyx:151)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at com.chaquo.python.PyObject.fromJavaNative(Native Method)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at com.chaquo.python.PyObject.fromJava(PyObject.java:87)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at com.chaquo.python.PyInvocationHandler.invoke(PyInvocationHandler.java:27)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at $Proxy4.onFileDescriptorEvents(Unknown Source)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at android.os.MessageQueue.dispatchEvents(MessageQueue.java:293)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at android.os.MessageQueue.nativePollOnce(Native Method)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at android.os.MessageQueue.next(MessageQueue.java:335)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:161)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at android.os.Looper.loop(Looper.java:288)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:7839)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
01-24 10:32:37.579 15673 15673 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
01-24 10:32:37.601 15673 15673 I Process : Sending signal. PID: 15673 SIG: 9
You can also reproduce this with the Toga GUI testbed. Unfortunately, you need to use an in-development version of Briefcase (app testing functionality hasn't landed in production yet), and a PR-version of Toga (adding coverage to a test suite) - and even then, some hand-modifications are required:
- Create and activate a virtual environment, and
pip install "git+http://github.com/beeware/briefcase#egg=briefcase"
- Clone the Toga repo associated with Remove Android test discovery workarounds beeware/toga#1747
- On line 88 of
testbed/pyproject.toml
, addtemplate_branch = "chaquopy-14.0.0"
- Comment out the workaround on L34-42 of
testbed/tests/testbed.py
- While in the
testbed
folder, runbriefcase run android --test
.
This will build and run the Toga Testbed app on an Android emulator in test mode. It will take a while to run, as it needs to download and configure a full Android SDK and related tooling; this is ~2GB of downloads. You should see a test suite running, then a coverage report, then the stack trace reported previously.
Expected behavior
Coverage shouldn't raise an IndexError in this situation.
Additional context
I believe the IndexError can be safely ignored - it's only encountered at shutdown, in a block of code that is doing cleanup.
I've been able to fix by patching Coverage.py to catch and ignore the IndexError, and that doesn't appear to have any ill effects. The workaround in L34-42 is an alternative workaround that can use a published version of Coverage.