Home

Oct. 21, 2014, 2 min read

Pytest inside 3ds Max

I recently started using the new Python API for 3ds Max with its MaxPlus module and had to write some tests for a python module that is used to manipulate a scene in Max. The tests must be run inside a 3ds Max environment, so by doing it from commandline I would have to call 3ds Max with a launch script that runs the tests. As starting the application takes quite some time, this is not a great choice for frequently running tests. Instead what I did was to call the tests directly with a button click in a running 3ds Max instance (it's open all day on my machine anyway).

As installing pytest for the 3ds Max Python interpreter would most likely mean to copy an existing installation folder manually, instead I preferred to inject the module by adding it to the sys.path.

UPDATE: The recent pytest version (2.9.2) is not compatible anymore, unless we introduce a small patch. Gladly we can do it on our side, without touching the pytest code itself. Our sys.stdout simply requires an extra method, otherwise we will fail with an AttributeError. Do the following before importing pytest:

def patch_std_out():
    """Add a mocked method to satisfy pytest."""
    def _isatty(*args, **kwargs):
        return False

    sys.stdout.isatty = _isatty

patch_std_out()

How to run the tests now? Prepare a Python launch file like:

# Inject pytest.
import sys
sys.path.insert(0, r"path-to-python-installation-with-pytest\Lib\site-packages")

# Modify args to prevent freezing of 3ds Max (due to filedescriptor error).
import pytest
args = [r"path-to-folder-containing-test-files", "-vv", "--capture=sys"]

# Run all tests.
pytest.main(args)

Afterwards execute the file from a macroscript like:

macroscript RunTests (
    python.ExecuteFile @"path-to-launch-file.py"
)

All pytest output as well as any Maxscript output written in the process will end up in your Maxscript listener as the tests are run one by one. Compared to the custom test runner I was using before this is just so much better.