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.

The tear-down should not be necessary. In any case, the del loop in the following code makes no sense, and deleting an attribute before assigning it doesn’t serve any purpose either.

Now that I know it’s a timer event, that narrows down where I have to look for the bug. Can you give any hints on what kind of observer it is? Mainly, I’m wondering if the AddObserver was called from Python, or if it was called from within VTK itself.

If it helps here’s a git grep AddObserver for my project:

Virgo.py:        self.AddObserver("CharEvent", self.onChar)
Virgo.py:        self.AddObserver(vtk.vtkCommand.EndInteractionEvent, self.StoreRelativeCameraInfo, 0.0)
Virgo.py:        self.cameras['foreground'].AddObserver("ModifiedEvent", self.sync_cameras)
Virgo.py:        self.interactor.AddObserver("RightButtonPressEvent", self.on_right_click)
Virgo.py:        self.interactor.AddObserver("TimerEvent", self.on_timer)
Virgo.py:        self.interactor.AddObserver("KeyPressEvent", self.on_key_press)
VirgoSplash.py:        self.interactor.AddObserver("TimerEvent", lambda obj,event: (

Both of these files are included here in case seeing the context around it would be helpful.

Virgo.py (67.9 KB)

VirgoSplash.py (3.7 KB)

This project is intended to be open source under NASA · GitHub but I’m running into some obstacles in the official software release process which is slowing it down. That said I can probably hack something together in a temporary area in GitHub to provide a state of code where I can reliably replicate this - let me know if that would be useful for you in your efforts. Thanks!

I’ve made a reproducer, it’s not minimal but it’s something that I can use as a starting point for debugging. Interestingly, it only crashes on macos, I haven’t been able to get it to crash on linux.

See the code in the expandable section below. Closing the window that it creates will sporadically cause a crash, causing a stack trace like the one you posted above. And disabling Python gc stops the crash.

from vtkmodules.vtkFiltersSources import (
  vtkCylinderSource,
)
from vtkmodules.vtkRenderingCore import (
  vtkActor,
  vtkCamera,
  vtkPolyDataMapper,
  vtkProperty,
  vtkRenderer,
  vtkRenderWindow,
  vtkRenderWindowInteractor,
)
import vtkmodules.vtkRenderingOpenGL2
import vtkmodules.vtkInteractionStyle
import vtkmodules.vtkRenderingUI

src = vtkCylinderSource()

mapper = vtkPolyDataMapper()
mapper.SetInputConnection(src.GetOutputPort())

actor = vtkActor()
actor.SetMapper(mapper)

actor.GetProperty().EdgeVisibilityOn()
actor.GetProperty().SetLineWidth(5)
actor.GetProperty().SetOpacity(0.5)

ren = vtkRenderer()
ren.AddActor(actor)
ren.SetBackground(0.1, 0.2, 0.4)

renWin = vtkRenderWindow()
renWin.AddRenderer(ren)
renWin.SetSize(500, 500)

iren = vtkRenderWindowInteractor()
iren.SetRenderWindow(renWin)
iren.Initialize()

def on_timer(caller, event):
    caller.Render()
    ren.GetActiveCamera().Azimuth(1)

iren.AddObserver("TimerEvent", on_timer)

timer_id = iren.CreateRepeatingTimer(20)

iren.Start()

# repeating the code causes sporadic crashes on macos

src = vtkCylinderSource()

mapper = vtkPolyDataMapper()
mapper.SetInputConnection(src.GetOutputPort())

actor = vtkActor()
actor.SetMapper(mapper)

actor.GetProperty().EdgeVisibilityOn()
actor.GetProperty().SetLineWidth(5)
actor.GetProperty().SetOpacity(0.5)

ren = vtkRenderer()
ren.AddActor(actor)
ren.SetBackground(0.1, 0.2, 0.4)

renWin = vtkRenderWindow()
renWin.AddRenderer(ren)
renWin.SetSize(500, 500)

iren = vtkRenderWindowInteractor()
iren.SetRenderWindow(renWin)
iren.Initialize()

iren.AddObserver("TimerEvent", on_timer)

timer_id = iren.CreateRepeatingTimer(20)

iren.Start()

Edit: I’ve found a fix that I can merge before the VTK 9.6 release. For now, you can work-around by ensuring that you call interactor.DestroyTimer(timerId) for every timer that you create before the interactor goes out of scope and destructs. Don’t do RemoveAllObservers() because VTK sometimes adds its own observers for its own purposes.

Thanks so much for the guidance I’ll give it a shot!

Update - I backed out gc.disable() and added just interactor.DestroyTimer(timerId) as a “tear down” mechanism for my top level class - this definitely helped as I struggled to replicate the InvokeEvent error for quite some time. I will note that after 116 retries of the test I did get a single segmentation fault but couldn’t capture the stack trace as it wasn’t instrumented during that setup. I ended up moving forward turning all my unit tests back on and by keeping gc.disable() at the top of my test I have now executed a few thousand tests without a single issue wrt memory.

So there may still be an issue but it’s much more rare with your suggestions implemented and completely avoidable with python garbage collection disabled. I’ll be happy to re-test when the newest VTK release is available and provide more detail then if I can still reproduce the error.

Thanks David this really helped push my project in the forward direction!

That’s good to hear. If you’re curious about the fix, it’s !12636 and it has been merged into the master branch.

It would be nice if you could eventually remove the gc.disable(), because if there are any remaining memory issues, I’d rather see them fixed, instead of hidden.