- pytest-xdist
- pytest-xdistでのsession scope fixtureの管理
- pytest-parallel
- 各種状態でのPID, PPID
- pytest-parallelでのsession scope fixtureの管理
- pytest-benchmarkとの関連
- まとめ
pytest-xdist
pytestはPythonのテストを行うフレームワークですが、 デフォルトではテストを一個ずつ行います。
並列で動かすためには別途プラグインを入れる必要があり、pytestのチームが作っているものとして pytest-xdist というプラグインがあります。
これをインストールすると-n
(--numprocesses
)というオプションが使えるようになり
これで指定した数だけ使うマルチプロセスでテストを行うようになります。
-n auto
とすれば環境で使えるCPUの数だけプロセスを作ります。
設定ファイルなどでこの設定をしてしまっている場合に通常のシングルプロセスにしたい場合には
-n 0
とゼロを指定します。
-n 1
も同じようにシングルプロセスになりますが、実際にはpytest
のプロセスの下に
子プロセスを1個作って実行するようになるので-n 0
とは厳密には違うものになります。
pytest-xdistでのsession scope fixtureの管理
xdistではマルチプロセスとして各テストを行う際、 指定された数のプロセスを作りそれぞれでセッションを行うような形でテストを実行します。
つまり、pytestにおけるsession scope fixtureを作った際、
プロセスの分だけそのfixtureが実行されます。(autouse=True
とするか各プロセス内のいずれかのテストがそれを呼んだとするか、各プロセスに最低一個そのfixtureを呼ぶテストが含まれるとして。)
以下のようなテストファイルを考えます。
setup
というautouse=True
(各テストから呼ばれなくても必ず実行される)のfixtureを持ち、
4つのテストを持つテストファイル。
(何一つテストしてませんがfixtureを見るためだけのものとして。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
setup
ではworker_id
というfixtureを引数に持っていますが、これは
pytest-xdistをインストールすると使えるようになり、
pytest-xdistが無効な場合にはmaster
という文字列を返し、
有効になるとgw0
, gw1
などプロセスごとのIDを返します。
sys.stdout = sys.stderr
としてるのはxdistでマルチプロセスにすると-s
で標準出力が出力されなくなるため
1。
また、print
出力でpytestの標準出力と簡単に分けて分かりやすくもなります。
通常通り実行してみると、
1 2 3 4 5 6 7 |
|
こんな感じでsetup
は全体で一回だけ呼ばれていることが分かります。
これを2つのプロセスでやってみると
1 2 3 4 5 6 7 8 9 |
|
今度はworker_id
がgw0
, gw1
というものが出来ていて、それぞれで一回ずつsetup
が呼ばれているのが分かります。
session scopeなfixtureは単に一回やってその情報をプロセスに渡すのに何度も生成する無駄を省くこともありますが、 外部データなどの準備で複数回実行されると困るものもあるかと思いますが。
そのような際にはこのままだと困りますが、そういった場合には setupを以下のように書き換えて挙げればOK。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
pytest-xdistが無効(worker_idがmaster
)のときは全く同じ動作、
有効なときは
checkという共通ファイルがあるかないかで動作を変更して、
最初のプロセスでそのファイルを作り、
更にfilelock(pytest-xdistの依存するライブラリになってるのでpytest-xdistをインストールすればインストール済)を使ってこの準備部分を排他的に処理しています。
これによって
1 2 3 4 5 6 7 8 9 10 11 |
|
こんな感じでgw1
ではfirst setup
が実行され、gw0
ではsetup was done
が実行されます。
-n 4
とかにするとsetup was done
のプロセスが増えます。
メモリ上で直接渡す簡単な方法は用意されてないので、もしなにかデータを生成して共通のものを使いたいなら
最初のプロセスでfn
に書き込んで後のプロセスではここから読み出す、みたいな方法が考えられます。
共通のファイルを作るために
1
|
|
のようにディレクトリを取得しています。
tmp_path_factory
はpytestにもともとあるfixtureでgetbasetemp()
で
そのプロセスで使う一時ディレクトリを取得できます。
通常シングルプロセスでは$TMPDIR/pytest-of-$USER/pytest-<N>のようなディレクトリで、
N
を実行する毎にインクリメントして毎回別のディレクトリを使います。
これがxdistでマルチプロセスにすると、各プロセスごとに $TMPDIR/pytest-of-$USER/pytest-<N>/popen-gw0 のようにもう一段階掘ったディレクトリが割り当てられます。
ここでpytest-N
までの部分は共通なのでgetbasetemp().parent
によってこの共通ディレクトリを
取得してそこを使ってデータファイルを作ったりロックファイルを置いたりすることができます。
pytest-parallel
pytest-xdistと 似たようなプラグインでpytest-parallelというものもあります。
こちらは--workers 4
といった感じでプロセス数(ワーカーノード数)を変更することが可能です。
ちょっと注意が必要なのが普通にpytest-parallelだけを入れて実行しようとするとpyという ライブラリが無くて
1
|
|
といったエラーを吐きます。
上のIssueではpytest-benchmarkを一緒に使おうとすると問題が出るとありますが、 python3.7以上だとpytest-benchmarkなしでも同様のエラーが出ます。
これはpip install py
と別途入れてあげれば回避出来ます。
pytest-parallelではpytest-xdistと違ってsys.stdout
も-s
で出力されます。
また、一番大きな違いは--tests-per-worker 2
とtests-per-worker
を設定するとその数分だけ
同じプロセス内でマルチスレッドでテストを行うようになります。
これによってテストで実行されるジョブがスレッドセーフなのかどうか、などを検証することが出来ます。
一方でマルチプロセスでもマルチスレッドでも各テストがすべて独立のセッションとして実行されるようになり、
autouse=True
なsession scope fixtureがあると全てのテストで実行されます。
またxdistのworker_idのようなfixtureもないのでマルチプロセス/マルチスレッドかどうか、を判断することはfixtureなどを使っては出来ません 2。
またtmp_path_factory
は各テスト毎に
$TMPDIR/pytest-of-$USER/pytest-<N>
というディレクトリを作る形になり、
xdistのときのようにparent
を取ると毎回同じディレクトリになってしまいます。
のでちょっと別の工夫をする必要があります。
各種状態でのPID, PPID
pytestを走らせたときに全体で共通のものを取得するために pytest-parallelではpytest自体のPIDを使うことが考えられます。
PIDをチェックするために以下のようなスクリプトを用意します。
注意として、今、環境に、pytest, pytest-xdist, pytest-parallelの全てがインストールされてるとします。
pytest-xdistが入ってないとworker_id
というfixtureが存在しないのでエラーになってしまいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
まずは通常のシングルプロセス。PPIDのチェックのため2回走らせます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
ppid
は同じになっていることがわかりますが、これはこのコマンドを実行しているシェルのPIDです。
pytest自体のPIDはos.getpid()
で取った最初の値です。
また、tmp_path_factory.getbasetemp()
は1つ数字が大きいディレクトリに移っていることが分かります。
次はpytest-xdist:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
今度は2回setupが呼ばれてますが、gw1
, gw0
共に同じppid
の値を持ってることが分かります。
ただし、別のpytest
実行時にはこの値は変わっています。
これはテストを実行しているプロセス(pid
のプロセス)がpytest
のプロセスの子プロセスになっていて、
4870
や5004
のppid
が示すプロセスがpytest
のプロセスになっているためです。
ここで試しに-n 0
と-n 1
を試してみると、
-n 0
は
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
と、-n
オプションを付けない通常の場合と同様ppid
はシェルのPIDでpid
がpytestのPIDになっています。
また、一時ディレクトリもpytest-<N>
です。
一方-n 1
は
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
こんな感じでppid
の値も変わっていて、マルチプロセスの時と同様、pytest
のプロセスが
子プロセスを作っってpid
にあたるプロセスの中でテストを実行していることが分かります。
一時ディレクトリもpopen-gw0
と1つ掘り下げた先になっています。
次に本題のpytest-parallelの場合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
まず、worker数2にしてますが、setupは4回呼び出されています。
ppid
の値を見ると各pytestをお実行で6538, 6869とすべて同じものが4回呼び出されています。
また、それぞれのppid
が同じもの同士で同じ一時ディレクトリを使っていることが分かります。
ただしそれらはpytest-<N>
の通常のpytestで作られるものと同じ階層です。
一方でpid
は最初の方だと6653と6652、2つ目だと6884, 6885の2種類があります。
それぞれが同じプロセスのPIDです。
(4つのテストで2プロセスですが、一瞬で終わるテストでタイミング的に1:3に分かれてしまっている。)
ここでppid
に注目すると同一のpytest内では共通で次のpytest実行時には別の数字になっているので
これが共通のデータファイルやlockファイル用に使えそうです。
--workers 1
としてもxdistのときと同様pytestのプロセスの下に子プロセスが生まれてその中でテストが実行されます。
ただし、pytest-parallelでは--workers 0
としてしまうと子プロセスが作られずに
ずっと待ち状態になってしまうので使えません。
マルチスレッドの場合を見てみると
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
|
こんな感じで、この場合はworker
を指定せずにシングルプロセスなので
pid
も全て同じになります。
ただし一時ディレクトリは各pytestで二種類あってそれぞれが別のスレッドに対応しているようです。
また、ppid
が2回めのpytest実行時には変わっていることからも各テストが実行されるプロセスは
pytestの子プロセスとして実行されていることが分かります。
この場合でもppid
を用いて共通ファイルを作れそうです。
pytest-parallelでのsession scope fixtureの管理
上のようにos.getppid()
の値を用いてディレクトリを作り、
そのにlockファイルなどを置くようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
これを実行すると
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
こんな感じでsetup
が4回、テストの数だけ呼ばれていることがわかりますが、
first setup
は一回だけになっています。
この場合lockファイルとかが置かれるのは $TMPDIR/pytest-of-$USER/<shell’s PPID>/のようなディレクトリになります。
このPPIDがpytest実行毎に毎回変わるのでxdistのところでやったようなことが出来ます。
ただし一時ディレクトリがあまり使われてない環境で繰り返しpytest
を実行すると
PPIDが被る可能性は十分あるので適度に手動でrm -rf $TMPDIR/pytest-of-$USER
する必要はあるかもしれません。
手動でやっているのであれば.bashrcの中とかでシェル起動時にやるとか。
また、このテストファイルを--workers
や--tests-per-worker
の引数なしで実行すると今度はPPIDが
このコマンドを実行したシェルのPIDとかになって、それは毎回一緒になってしまいます。
(なので$TMPDIR/pytest-of-$USER/<shell’s PPID>/checkを消さないと次以降setupの準備が一切行われないことになってしまう。)
なのでこのようなスクリプトにしてしまったらシングルプロセスとして使いたい場合にも--workers 1
(もしくは--tests-per-worker 1
)を与えてpytest-parallelを呼ばないといけません。
そもそもpytest-xdistとpytest-parallelを一緒に使うべきではありませんが、このスクリプトを-n 2
とかで実行すると
checkはpopen-gw0などのディレクトリの下に行くので今度は全てのプロセスでsetupが実行されてしまうことになります。
そんな感じでpytest-parallelでのsession scopeなど、関数より大きな単位でのfixtureを使おうと思うと pytest-xdist以上に面倒だったりします。
pytest-benchmarkとの関連
pytest-benchmark はpytestの中で関数などの実行速度のベンチマークを取ってくれるプラグインですが、 pytest-xdistやpytest-parallelが有効になっているとベンチマークテストが無効になります。
pytest-xdistなどを入れていると、通常のテスト時には何もせずにマルチプロセスでやって欲しいこともあると思いますが、
そういった場合pytest.iniやproject.tomlでaddopts = "-n auto"
みたいな設定を入れたりします。
そこでベンチマークを行おうとすると無効になってしまうので、ベンチマークを取る時だけ
1
|
|
みたいな感じでマルチプロセスを無効にすることでベンチマークを取ることができます。
ただ、pytest-parallelは上にも書いたように--workers 0
と0を指定できないので、
設定ファイル内で--workers auto
みたいにpytest-parallelを有効にしてしまうと
無効にする方法がありません。(--workers 1
でもプロセスは1個になるけどpytest-parallel自体は有効になってしまってベンチマークが動かない。)
従ってpytest-benchmarkとpytest-parallelを同時に使いたい場合にはマルチプロセス(もしくはマルチスレッド)な オプションは設定ファイルに書いておくことは出来ません。
まとめ
pytestでテストをマルチプロセス化したい場合には pytest-xdistやpytest-parallelといったプラグインがありますが、 そのまま実行するとsession scopeなfixtureなどが想定とは違う動作になるので ちょっと工夫する必要があります。
pytest-benchmarkがマルチプロセスなプラグインが有効だと動かない事も注意が必要です。
そのあたりの対処法を考えるとpytest-xdistの方が簡単に管理ができます。
スレッドセーフかどうかのテストなどを行いたい場合以外は 基本pytest-xdistの方が使い勝手が良いです。
一方でpytest-parallelはマルチスレッド化などxdistでは出来ない機能もあるので そのあたりの機能が必要な場合はpytest-parallelを試してみると良いかな、という感じです。
-
(ただし、実際に実行されるプロセス自体はその都度生成されるわけではなく連続してテストを扱っていきます。 session scope fixtureの中とかで
print(os.getpid())
とかするとプロセスの数だけ違うIDが表示されます。 ↩