Looking for guidance on writing python unit tests using python vtk

I am developing a python-VTK application and am noticing some unusual behavior in python unittest-based tests I am building alongside my application. I have noticed that tests that are defined in separate suites appear to interfere with each other and my theory is that the underlying python VTK functionality is using some global or otherwise shared information making the scoping of my python tests not as isolated as I’d like. Ultimately I’m looking for tips/guidance on how to correctly set up python unit tests that leverage python VTK in a safe and isolated manner.

I have a couple anecdotal examples, first I noticed that unit tests which pass when run once will sometimes fail if running them multiple times per python interpreter. I have found that after a dozen or so executions of the same test, I’d receive a runtime failure that looks like this (python VTK 9.5.1 on Mac):


* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x726f77656d61726e)
    frame #0: 0x000000010777f150 libvtkCommonCore.dylib`vtkSubjectHelper::InvokeEvent(unsigned long, void*, vtkObject*) + 44
libvtkCommonCore.dylib`vtkSubjectHelper::InvokeEvent:
->  0x10777f150 <+44>: ldp    x9, x8, [x0, #0x8]
    0x10777f154 <+48>: cmp    x9, x8, lsl #6
    0x10777f158 <+52>: b.ne   0x10777f194    ; <+112>
    0x10777f15c <+56>: cmn    x9, #0x1
Target 0: (Python) stopped.

Through much testing I discovered that this error can be avoided 100% of the time by turning off python garbage collection with import gc; gc.disable() at the top of my testing script. The effectiveness of this approach has led me to believe that there is something unsafe happening when the python script creates multiple full rendering pipelines and those python objects call underlying C++ libraries to deconstruct. Telling python to never clean up bypasses the issue.

Another example - I have two suites, one of which uses self.render_window.OffScreenRenderingOn() and the other does not. If both tests are run in the same python instance, I notice the offscreen rendering test actually renders to the screen when it shouldn’t. Only when both tests are in the same file will this occur.

I can work on getting a standalone example of this to share but in the meantime I’m curious if anyone out there has run into this or has any tips on isolation and memory safety when running python VTK based unit tests. Thanks!

For the runtime failures, do the stack traces always show InvokeEvent? I suspect that events are being invoked after the observer has destructed. This would explain why disabling gc helps. If you can confirm that InvokeEvent is always involved, I can dig deeper to find the cause.

Regarding the offscreen rendering, I’m not very familiar with that part of the VTK code, but I can imagine that it might involve some global state. VTK’s own test suite always runs each test in its own process.

Hi David - I really appreciate your help. Yes when I see this EXC_BAD_ACCESS it’s always a stack trace that looks like this:

  * frame #0: 0x000000011129b1b8 libvtkCommonCore.dylib`vtkSubjectHelper::InvokeEvent(unsigned long, void*, vtkObject*) + 148
    frame #1: 0x0000000199556fcc Foundation`__NSFireTimer + 104
    frame #2: 0x0000000197f67ba0 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 32
    frame #3: 0x0000000197f67860 CoreFoundation`__CFRunLoopDoTimer + 980
    frame #4: 0x0000000197f6739c CoreFoundation`__CFRunLoopDoTimers + 332
    frame #5: 0x0000000197f4d7a8 CoreFoundation`__CFRunLoopRun + 1848
    frame #6: 0x0000000197f4c9e8 CoreFoundation`CFRunLoopRunSpecific + 572
    frame #7: 0x000000019951cc78 Foundation`-[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 212
    frame #8: 0x00000001995903a4 Foundation`-[NSRunLoop(NSRunLoop) runUntilDate:] + 100
    frame #9: 0x000000011752b89c libvtkRenderingOpenGL2.dylib`vtkCocoaRenderWindow::Render() + 124
    frame #10: 0x0000000116f5fea4 vtkRenderingOpenGL2.cpython-311-darwin.so`PyvtkCocoaRenderWindow_Render(_object*, _object*) + 244
    frame #11: 0x000000010069ea18 Python`cfunction_call + 100
    frame #12: 0x0000000100654140 Python`_PyObject_MakeTpCall + 128
    frame #13: 0x00000001007302d8 Python`_PyEval_EvalFrameDefault + 41220
    frame #14: 0x0000000100734684 Python`_PyEval_Vector + 116
    frame #15: 0x00000001006575b4 Python`method_vectorcall + 168
    frame #16: 0x0000000100731f64 Python`_PyEval_EvalFrameDefault + 48528
    frame #17: 0x0000000100734684 Python`_PyEval_Vector + 116
    frame #18: 0x00000001006543e8 Python`_PyObject_FastCallDictTstate + 96
    frame #19: 0x00000001006bcc1c Python`slot_tp_call + 188
    frame #20: 0x0000000100654140 Python`_PyObject_MakeTpCall + 128
    frame #21: 0x00000001007302d8 Python`_PyEval_EvalFrameDefault + 41220
    frame #22: 0x0000000100734684 Python`_PyEval_Vector + 116
    frame #23: 0x00000001006575b4 Python`method_vectorcall + 168
    frame #24: 0x0000000100731f64 Python`_PyEval_EvalFrameDefault + 48528
    frame #25: 0x0000000100734684 Python`_PyEval_Vector + 116
    frame #26: 0x00000001006543e8 Python`_PyObject_FastCallDictTstate + 96
    frame #27: 0x00000001006bcc1c Python`slot_tp_call + 188
    frame #28: 0x0000000100654140 Python`_PyObject_MakeTpCall + 128
    frame #29: 0x00000001007302d8 Python`_PyEval_EvalFrameDefault + 41220
    frame #30: 0x0000000100734684 Python`_PyEval_Vector + 116
    frame #31: 0x00000001006575b4 Python`method_vectorcall + 168
    frame #32: 0x0000000100731f64 Python`_PyEval_EvalFrameDefault + 48528
    frame #33: 0x0000000100734684 Python`_PyEval_Vector + 116
    frame #34: 0x00000001006543e8 Python`_PyObject_FastCallDictTstate + 96
    frame #35: 0x00000001006bcc1c Python`slot_tp_call + 188
    frame #36: 0x0000000100654140 Python`_PyObject_MakeTpCall + 128
    frame #37: 0x00000001007302d8 Python`_PyEval_EvalFrameDefault + 41220
    frame #38: 0x0000000100725608 Python`PyEval_EvalCode + 168
    frame #39: 0x000000010077beac Python`run_eval_code_obj + 84
    frame #40: 0x000000010077be10 Python`run_mod + 112
    frame #41: 0x000000010077bc50 Python`pyrun_file + 148
    frame #42: 0x000000010077b6a0 Python`_PyRun_SimpleFileObject + 268
    frame #43: 0x000000010077b02c Python`_PyRun_AnyFileObject + 216
    frame #44: 0x0000000100797a9c Python`pymain_run_file_obj + 220
    frame #45: 0x00000001007973e0 Python`pymain_run_file + 72
    frame #46: 0x0000000100796cac Python`Py_RunMain + 704
    frame #47: 0x0000000100797dec Python`Py_BytesMain + 40
    frame #48: 0x0000000197ac2b98 dyld`start + 6076

When I first ran into this issue I ran it by a couple AIs which advised me to better manage tearing down VTK-managed objects myself. This led me to create a tear_down method that attempts to deconstruct everything I use in my top level python class, it looks like this:

    def tear_down(self):
        if self.controller.timer_id:
            self.interactor.DestroyTimer(self.controller.timer_id)
        for camera in self.controller.cameras:
            self.controller.cameras[camera].RemoveAllObservers()

        self.interactor.RemoveAllObservers()

        for renderer in self.renderers:
            self.renderers[renderer].RemoveAllObservers()

        # Mark for deletion
        del self.interactor
        self.interactor = None
        for renderer in self.renderers:
            del renderer

Calling the function however appeared to make the problem worse and would result in another type of error signature (in addition to the first one) that looks like this:

(lldb) thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x10000fe0028)
  * frame #0: 0x0000000106e7ff50 libvtkCommonCore.dylib`vtkObject::RemoveObserver(vtkCommand*) + 40
    frame #1: 0x0000000102e1e5a0 libvtkRenderingCore.dylib`vtkInteractorStyle::SetInteractor(vtkRenderWindowInteractor*) + 44
    frame #2: 0x0000000102e1e278 libvtkRenderingCore.dylib`vtkInteractorStyle::~vtkInteractorStyle() + 36
    frame #3: 0x000000010a81975c libvtkInteractionStyle.dylib`vtkInteractorStyleTrackballCamera::~vtkInteractorStyleTrackballCamera() + 12
    frame #4: 0x00000001014bce38 libvtkWrappingPythonCore3.11.dylib`vtkPythonUtil::~vtkPythonUtil() + 320
    frame #5: 0x00000001014bcb70 libvtkWrappingPythonCore3.11.dylib`vtkPythonUtilDelete + 28
    frame #6: 0x0000000100776a68 Python`Py_FinalizeEx + 1076
    frame #7: 0x0000000100796afc Python`Py_RunMain + 272
    frame #8: 0x0000000100797dec Python`Py_BytesMain + 40
    frame #9: 0x0000000197ac2b98 dyld`start + 6076

Backing this out seemed to completely remove this error signature - from this I concluded that 1. I clearly don’t know the safe way to deconstruct VTK objects in python and 2. AI will not be taking over the world with software anytime soon :stuck_out_tongue:

VTK’s own test suite always runs each test in its own process.

Thanks for this info - I could do as well on the python side if I absolutely had to.