みなさんこんにちは。最近は机をつくって天板を削ったり苺を育てるための機械をつくったりと、休みの日は工作に励んでいます。普段はソフトウェアばっかり触っているので、手に触れるものをイチから自分で設計してつくるのは楽しいなぁ~と改めて感じます。

記事の目次

    Mayaでの実行結果をJenkinsに反映する

    前回までで、JenkinsからMayaを立ち上げ、起動スクリプトを実行してMayaを終了できるようになりました。今回は、Maya上でのテストの実行や、テスト結果とJenkinsとの連携を進めていきます。

    現時点で、JenkinsからMayaを実行して、scriptJobのPostSceneReadでMayaが起動し、シーンを読み込んだ後にプログラムを実行できるようになっています。JenkinsでMayaの実行結果を集計するためには、Mayaの処理結果をJenkinsに戻す必要があります。現時点でもMayaからの返り値は取得できる (cmds.quit(force=True, exitCode=0)) ものの、これだけでは処理に成功したか失敗したか程度のことしかわからず、テストの実行結果の詳細などはわかりません。

    テストの実行や結果の収集、Jenkinsへの反映に関してはpython単体で動作させる場合は便利な機能が用意されていて、mayapyも同じように使用できるケースが多いです。ただ、Maya経由で実行する場合は、起動時の引数がpythonと異なっていたり、userSetup.py経由でコードを実行するなど様々なちがいがあるため、これらの既存のしくみをそのまま使用することが難しいです。そのため、足りない部分を自分で用意する必要があります。

    Mayaからpytestを実行する

    はじめに、Mayaからpytestを実行できるようにします。ひとまずMayaのScript Editorから実行してみます。algorithmを開発するためにvenvを作成してあるので、venv中でactivateした環境からMayaを立ち上げるとvenv中にインストールしたパッケージも使用できます。

    C:\Users\chiyama\repo\algorithm>venv\Scripts\activate.bat
    (venv) C:\Users\chiyama\repo\algorithm>"C:\Program Files\Autodesk\Maya2023\bin\maya.exe"

    Mayaが立ち上がったらScript Editor上でpytestを実行します。

    import pytest
    pytest.main()

    すると、エラーになってしまいます。



    # INTERNALERROR> Traceback (most recent call last):
    # INTERNALERROR>   File "C:\Users\chiyama\repo\algorithm\venv\Lib\site-packages\_pytest\main.py", line 264, in wrap_session
    # INTERNALERROR>     config._do_configure()
    # INTERNALERROR>   File "C:\Users\chiyama\repo\algorithm\venv\Lib\site-packages\_pytest\config\__init__.py", line 996, in _do_configure
    # INTERNALERROR>     self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
    # INTERNALERROR>   File "C:\Users\chiyama\repo\algorithm\venv\Lib\site-packages\pluggy\_hooks.py", line 277, in call_historic
    # INTERNALERROR>     res = self._hookexec(self.name, self.get_hookimpls(), kwargs, False)
    # INTERNALERROR>   File "C:\Users\chiyama\repo\algorithm\venv\Lib\site-packages\pluggy\_manager.py", line 80, in _hookexec
    # INTERNALERROR>     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
    # INTERNALERROR>   File "C:\Users\chiyama\repo\algorithm\venv\Lib\site-packages\pluggy\_callers.py", line 60, in _multicall
    # INTERNALERROR>     return outcome.get_result()
    # INTERNALERROR>   File "C:\Users\chiyama\repo\algorithm\venv\Lib\site-packages\pluggy\_result.py", line 60, in get_result
    # INTERNALERROR>     raise ex[1].with_traceback(ex[2])
    # INTERNALERROR>   File "C:\Users\chiyama\repo\algorithm\venv\Lib\site-packages\pluggy\_callers.py", line 39, in _multicall
    # INTERNALERROR>     res = hook_impl.function(*args)
    # INTERNALERROR>   File "C:\Users\chiyama\repo\algorithm\venv\Lib\site-packages\_pytest\terminal.py", line 228, in pytest_configure
    # INTERNALERROR>     reporter = TerminalReporter(config, sys.stdout)
    # INTERNALERROR>   File "C:\Users\chiyama\repo\algorithm\venv\Lib\site-packages\_pytest\terminal.py", line 330, in __init__
    # INTERNALERROR>     self.isatty = file.isatty()
    # INTERNALERROR> AttributeError: 'maya.Output' object has no attribute 'isatty'

    Mayaのsys.stdoutがpython標準のものと異なっているため、きちんと動かないようです。Mayaのstdin/stdout周りは何かと問題の起きやすいポイントなので、エラーを見て対応します。今回はuserSetup.py上でsys.stdoutをラップしてしまいます。

    import sys
    import maya.cmds as cmds
    import maya.utils
    
    from PySide2 import QtWidgets
    
    job_id = None
    launched = False
    
    
    class StdOutWrapper:
        def __init__(self, stdout):
            self._stdout = stdout
    
        def __getattr__(self, item):
            if item == 'isatty':
                return self.isatty
            else:
                return getattr(self._stdout, item)
    
        def isatty(self):
            return False
    
    
    def run_test():
        global launched
        if launched is True:
            return
    
        print('==== run_test for algorithm ====')
        launched = True
    
        # sys.stdout を差し替える    
        sys.stdout = StdOutWrapper(sys.stdout)
    
        maya.utils.executeDeferred(lambda:cmds.scriptJob(kill=job_id))
        
        print(sys.version_info)
    
        QtWidgets.QMessageBox.information(None, 'userSetup', 'algorithm')
    
        # sys.stdout を元に戻す
        sys.stdout = sys.stdout._stdout
    
    cmds.quit(force=True, exitCode=0)
    
    
    job_id = cmds.scriptJob(event=('PostSceneRead', run_test))

    このようにしてsys.stdoutをStdOutWrapperに差し替えて、isattyをこの中で解決します。Script Editorから実行する場合はSdtOutWrapperの定義とsys.stdoutを差し替える部分を事前に実行しておきます。

    再度pytestを実行してみます。

    import pytest
    pytest.main()
    
    ============================= test session starts =============================
    platform win32 -- Python 3.9.7, pytest-7.1.3, pluggy-1.0.0
    rootdir: C:\Users\chiyama\repo\algorithm, configfile: pytest.ini
    collected 0 items / 1 error
    
    =================================== ERRORS ====================================
    ____________________ ERROR collecting tests/test_search.py ____________________
    ImportError while importing test module 'C:\Users\chiyama\repo\algorithm\tests\test_search.py'.
    Hint: make sure your test modules/packages have valid Python names.
    Traceback:
    C:\Program Files\Autodesk\Maya2023\Python\lib\importlib\__init__.py:127: in import_module
        return _bootstrap._gcd_import(name[level:], package, level)
    tests\test_search.py:6: in <module>
        from algorithm import search
    E   ModuleNotFoundError: No module named 'algorithm'
    =========================== short test summary info ===========================
    ERROR tests/test_search.py
    !!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
    ============================== 1 error in 0.06s ===============================

    pytestは実行されましたが、algorithmがないと言われてしまいます。理由はわからないですが、'pip install -e' で登録する方法だと認識できていないようです。こういう場合はあまり深く考えず、sys.pathにパスを追加してしまいます。

    import pytest
    pytest.main()
    
    ============================= test session starts =============================
    platform win32 -- Python 3.9.7, pytest-7.1.3, pluggy-1.0.0
    rootdir: C:\Users\chiyama\repo\algorithm
    collected 1 item
    
    tests\test_search.py .                                                   [100%]
    
    ============================== 1 passed in 0.02s ==============================
    # Result: <ExitCode.OK: 0>

    無事にMayaからpytestが実行できるようになりました。本来ならきちんと原因を追究して対応したほうが良いものの、Maya上で動かすとこういった細かいところで謎な挙動をすることがたまにあるので、あんまり気にしてもしょうがないなぁというのが個人的な感想です。

    最終的にできたuserSetup.pyは以下のようになりました。パスの追加はJenkinsから実行する際に行います。

    import os
    import sys
    import maya.cmds as cmds
    import maya.utils
    
    from PySide2 import QtWidgets
    
    job_id = None
    launched = False
    
    
    class StdOutWrapper:
        def __init__(self, stdout):
            self._stdout = stdout
    
        def __getattr__(self, item):
            if item == 'isatty':
                return self.isatty
            else:
                return getattr(self._stdout, item)
    
        def isatty(self):
            return False
    
    
    def run_test():
        global launched
        if launched is True:
            return
    
        print('==== run_test for algorithm ====')
        launched = True
    
        # sys.stdout を差し替える    
        sys.stdout = StdOutWrapper(sys.stdout)
    
        import pytest
        pytest.main([os.getcwd()])
    
        # sys.stdout を元に戻す
        sys.stdout = sys.stdout._stdout
        cmds.quit(force=True, exitCode=0)
    
    
    job_id = cmds.scriptJob(event=('PostSceneRead', run_test))

    pytestの結果を収集する

    pytestの結果をJenkinsで活用できるようにログファイルを出力するようにします。JenkinsではJUnit形式のテスト結果を集計する機能があるので、そのためのファイルを出力します。pytestの挙動はpytest.main() 実行時の引数で指定することもできますが、設定ファイルでまとめてあった方が管理が楽なのでpytest.iniを作成します。

    [pytest]
    addopts = --junit-xml=results.xml

    これで、pytestを実行した結果をJUnit形式でresults.xmlに出力するようになります。pytest.iniをsetup.pyやuserSetup.pyと同じ場所に置いて、忘れずにリポジトリに追加しておきます。

    Jenkinsへの組み込み

    ここまでできたら、Jenkinsで実行できるようにします。Maya起動部分でalgorithmが見つからなかったので、Jenkins上でもPYTHONPATHに登録しておきます。

    call C:\Users\chiyama\.jenkins\jobs\mayatest\venv\Scripts\activate.bat
    
    set MAYA_UI_LANGUAGE=en_US
    set PYTHONPATH=C:\Users\chiyama\.jenkins\jobs\mayatest\venv\Lib\site-packages;%PYTHONPATH%
    set PYTHONPATH=%WORKSPACE%\src;%PYTHONPATH%
    set PYTHONPATH=%WORKSPACE%;%PYTHONPATH%
    
    "C:\Program Files\Autodesk\Maya2023\bin\maya.exe" %WORKSPACE%\userSetup.ma

    また、ビルド後の処理の追加で、JUnitテスト結果の集計を追加します。

    テスト結果XMLに、results.xmlを指定します。設定ができたら保存します。ここまでできたら、何かコードを変更してpushしてみましょう。正常に処理が行われるとプロジェクトページにテスト結果が表示されるようになります。

    また、最新のテスト結果へのリンクを開くと詳細を確認できます。

    まとめ

    これで、gitリポジトリにコードをpushするとJenkinsがMayaを起動してテストを実行し、その結果を集計してJenkins上で表示できるようになりました。ここまでくればMaya特有の問題はあらかた片付いたことになるので、Jenkinsを使用したCI/CD環境の熟成に集中できるようになります。

    第49回の公開は、2022年12月を予定しております。

    痴山紘史

    日本CGサービス(JCGS) 代表

    大学卒業後、株式会社IMAGICA入社。放送局向けリアルタイムCGシステムの構築・運用に携わる。その後、株式会社リンクス・デジワークスにて映画・ゲームなどの映像制作に携わる。2010年独立、現職。映像制作プロダクション向けのパイプラインの開発と提供を行なっている。新人パパ。娘かわいい。

    TEXT_痴山紘史 / Hiroshi Chiyama(日本CGサービス
    EDIT_尾形美幸 / Miyuki Ogata(CGWORLD)