Coverage.py 利用ノート

はじめに

本稿の目的は Python パッケージ Coverage.py について次の事項を達成することだ:

  • このパッケージの目的や機能を理解する

  • このパッケージを自分のローカル環境にインストールする方法を会得する

  • このパッケージの CLI の基本的なコマンドの動作を理解する

  • このパッケージの構成方法を会得する

  • このパッケージの機能を自分のプロジェクトに組み込む方法を学ぶ

Coverage.py の機能

Coverage.py は Python プログラムのコード網羅度を測定する道具だ。プログラムを監視し、コードのどの部分が実行されたかを記録した後、ソースを解析して実行可能であったが実行されなかったコードを特定する。この測定は通常、テストの効果を評価するために使用する。テストによってコードのどの部分が実行されているか、またはいないかを示すことができる。

  • 行網羅を測定するのが基本機能。

  • 分岐網羅を測定できる。

  • どのテストがどの行を実行したかを示せる。

  • レポートを HTML, XML, LCOV, JSON で生成できる。

  • API がある。

インストール・更新・アンインストール

Coverage.py をインストールする情況は二つ考えられる:

  1. 与えられた Python プロジェクトが規定する依存関係の一部としてインストールする

  2. 個人の Python 環境に直接インストールする

最初の場合は当該プロジェクトが用いる依存関係管理ツールを実行することで自動的に仮想環境にインストールされるのが通例だ。最後の場合は好みのパッケージ管理ツールを使用してインストールできる。私は mamba を愛用しているので、その用例を次に示す:

mamba を使用してインストールする
$ mamba install coverage

この手のパッケージ管理ツールを使用する場合にパッケージ名 coverage を指定することが原則だ。

インストール直後に Coverage.py が正しくインストールされたことを次に示すコマンドラインで確認しろ。特に、開発公式が推奨しているように、C 拡張が用いられていることを確認しろ。

バージョンと C 拡張が用いられていることを確認する
$ coverage --version # or python -m coverage --version
Coverage.py, version 7.13.1 with C extension
Documentation at https://coverage.readthedocs.io/en/7.13.1
You can also invoke coverage.py as a module:

更新およびアンインストール手順は、使用するパッケージ管理ツールに依存して定まる。どちらの場合にも、インストール時に指定したパッケージ名を指定する必要がある。

Todo

関連ノート一覧

操作

いちばん簡単な用例

Python スクリプト helloworld.py の網羅度を Coverage.py に測定させ、データを観察するには次のようにする:

スクリプトの網羅度を調べる
$ coverage run helloworld.py
Hello, World!
$ coverage report -m
Name            Stmts   Miss  Cover   Missing
---------------------------------------------
helloworld.py       1      0   100%
---------------------------------------------
TOTAL               1      0   100%

このスクリプトは条件分岐も例外処理もない、関数 print 呼び出し一行のみからなるものなので、上のように簡素な統計が示される。実践開発では、スクリプトの内容が複雑であったり、対象がパッケージであったり、コードの内容が並行処理や非同期処理を含み、複雑なプログラム進行をする。このような単純な数値にはならない。また、オプションを付加したり、適当な文脈を定義して測定を複数実行してデータを比較考量することが業務では求められるだろう。

Tip

実行形式 coverage にパスを通していない場合には、代わりに python -m coverage からコマンドを開始すればよい。以下同様。

測定コマンド

対象コードから網羅度を抽出するにはコマンド coverage run を実行する。この結果、既定では測定データをファイル .coverage に出力する。このファイルは報告コマンドが機能するのに欠かせない。

測定対象はスクリプト形式であってもモジュール形式であってもよい。コマンドを次のようにする:

  • スクリプトならば coverage run script.py

  • モジュールならば coverage run -m module

対象 Python プロジェクトが採用しているテストフレームワークによって測定コマンドが決まることも多い。テストランナーによってはテスト実行中に Coverage.py を容易に使用できるように統合装置を設けているものがある。たとえば、pytestpytest-cov プラグインを利用可能にしている。このプラグインが Coverage.py を裏で利用する。

  • pytest の場合は coverage run -m pytest ...

    • さらに pytest-cov を用いる場合には coverage run -m pytest --cov ...

  • 標準パッケージ unittest を直接利用する場合は coverage run -m unittest discover

観察コマンド

測定データファイルが生成されている状態で報告コマンドを実行すると、コード網羅度を観察することができる。いちばん簡単な用例で見た形式以外にも、データ分析を助ける多様な形式が用意されている。主なものを以下に示す:

coverage report

いちばん簡単な用例で示したように、プレインテキストによる表形式で出力する。対象が単純な Python プログラムである場合に使いやすい。

coverage html

HTML ファイルに結果をまとめる。フィルターやソート機能が JavaScript で実装されている。もっとも潰しが効く形式。

この方式で報告書を出力すると、既定ではディレクトリー htmlcov に HTML ファイルや付属ファイルを生成する。

報告書を HTML ファイルとして生成するコマンドの実行例
$ coverage html
Wrote HTML report to htmlcov/index.html

開発公式による 出力例 <sample-coverage> を見るといい。

coverage xml

XML ファイルに結果をまとめる。開発者自身が測定データを整形したい場合に有用と考えれる。

coverage json

JSON ファイルに結果をまとめる。おそらく上級開発者が jq などで高度な問い合わせをして利用すると考えられる。

coverage lcov

LCOV ファイルに結果をまとめる。

UNIX/Linux には genhtml という道具があり、このコマンドを利用することでも、上述の coverage html と同等のことができるそうだ。

その他のコマンド

コマンド coverage help を実行すると、利用可能なコマンド一覧を表示する。

コマンド coverage debug topic を実行すると Coverage.py の内部状態を表示する。バグ報告のときにこの出力を提出する。

コマンド coverage erase を実行すると、ファイル .coverage などの測定データを削除する。

コマンド coverage combine を実行すると、複数の測定データファイルを一つに結合する。測定データが複数あり得る理由は、測定が複数の相異なるコンテキストで実施されることがあるからだろう?

構成・カスタマイズ

Coverage.py の動作をコマンドライン以外から制御する方法をいくつか記す。

環境変数

コマンド coverage run --help を実行して [env: COVERAGE_XXXX] というパターンを探すと、Coverage.py が参照する主要環境変数が確かめられる。

COVERAGE_DEBUG

コマンドラインオプション --debug=OPTS を備えるコマンドに対して、それをコマンドラインで指定するのと同様の効果がある。

COVERAGE_DEBUG_FILE

デバッグ情報を出力するファイルパスを指定する。これを指定しない場合(既定)、構成ファイルで対応する項目もまた無指定であれば、Coverage.py は標準エラーに情報を出力する。

COVERAGE_FILE

コマンドラインオプション --data-file=DATA_FILE と同様に、測定データ保存先ファイルを指定する。

COVERAGE_PROCESS_START

コマンド coverage run を実行する方法のほかに、API を用いて Coverage.py に測定をさせる方法がある。その際に、この環境変数は構成ファイルのパスとして扱われるらしい:

Coverage.py includes a function designed to be invoked when Python starts:
coverage.process_startup(). It examines the COVERAGE_PROCESS_START
environment variable, and if it is set, begins coverage measurement. The
environment variable’s value will be used as the name of the configuration
file to use.

この関数呼び出しを含む Python コードが記された .ph ファイルを用意するのが欠かせない。

Todo

ここは理解が十分でないので後で見直す。

COVERAGE_RCFILE

コマンドラインオプション --rcfile=FILE と同様に、構成ファイルの在り処を指定する。

COVERAGE_RUN

測定実施中に Coverage.py がこの環境変数を設定するようだ。一般使用者はこの環境変数をアクセスしてはいけないと考えられる。

構成ファイル

コマンド coverage run --help を実行してオプション --rcfile=RCFILE の記述を読むと、構成ファイルとして処理されるファイルの優先順を確かめられる:

  1. .coveragerc

  2. .coveragerc.toml

  3. setup.cfg

  4. tox.ini

  5. myproject.toml: TOML が使用可能な場合にこの優先一覧に現れる。

例えばファイル myproject.toml で構成する場合、コマンドのオプションを指定するセクションの名前は [tool.coverage.command] だ。ここにオプションの名前と値を TOML 形式で記述するのだ。

常用するコマンドラインオプションがある場合、それらに対応する構成ファイル項目を定義しておけば便利だと考えていた。Coverage.py の動作はプロジェクトごとに決定しがちだ。対話シェルの構成ファイルのように、~/.coveragerc のようなユーザーレベルの構成ファイルのようなものを設ける意義はないようだ。

使用方法・コツ

測定対象を細かく指定する

測定コマンドと報告コマンドの両方において、次のオプションを適切に組み合わせることで、Coverage.py の測定対象ファイルを限定することが可能だ:

  • --source=SRC1,SRC2,...

  • --include=PAT1,PAT2,...

  • --omit=PAT1,PAT2,...

コツなど:

  • 当然だが、このオプション群のうちの一つだけを用いてもよい。

  • includeomit を当時に与えると、まずファイルの集合を include が指示するものに限定し、その後 omit が指示するファイルをその集合から除去する。この集合演算の結果集合が測定対象ファイルの全てとなる。

  • sourceinclude を同時に与えると、include の指定が無視される(警告が出る)。

  • パターンには Bash 式のワイルドカード ?, *, ** が使用できる。

コード片を測定対象にさせない方法

ファイル丸ごととまではいかずとも、測定前に実行されないことが分かっているコードが存在する場合、そのコードを網羅測定から排除できる方法がある:

  • # pragma: no cover コメントを測定から除外するコードに付加する。

  • 構成ファイル report セクションにおいて、オプション exclude_also に除外するコードを示す正規表現パターンを定義する。正規表現はステップの一部に合致すれば十分だ。

報告割愛オプション

報告コマンド各種には測定結果の一部を省略するオプションが二つある。

--skip-empty

測定対象ファイルのうち、実行可能行がゼロであるものを報告から省く。コマンド report, html, xml で使用可能。

--skip-covered, --no-skip-covered

測定対象ファイルのうち、網羅度百%のものを報告から省くか否かを指示する。コマンド reporthtml で使用可能。

網羅度が期待するよりも低いようならば報告全体を取りやめるオプションもある:

--fail-under=MIN

総網羅度が MIN 未満の場合、ステータスコード 2 で終了する。百分率で指定。

分岐網羅

分岐網羅はテストが実行したプログラム的分岐の度合いを計測する指標だ。Coverage.py で分岐網羅を測定するには、測定コマンドにオプション --branch を明示的に付加するのが不可欠だ。これにより、測定コマンドは通常の測定に加え、分岐網羅も測定する。

分岐網羅実行なしとありの測定結果の比較
$ coverage run branch.py
$ coverage report -m
Name        Stmts   Miss  Cover   Missing
-----------------------------------------
branch.py       5      0   100%
-----------------------------------------
TOTAL           5      0   100%
$ coverage run --branch branch.py
$ coverage report -m
Name        Stmts   Miss Branch BrPart  Cover   Missing
-------------------------------------------------------
branch.py       5      0      2      1    86%   8->10
-------------------------------------------------------
TOTAL           5      0      2      1    86%

コンテキスト

Todo

コマンド coverage combine を先に述べる。

網羅度測定を複数の条件を設けて、それぞれの条件下で測定を実施することができる。それがコンテキスト機能だ。測定とコンテキスト記録を一緒にするものだ。

  • 静的コンテキスト:実行一周で固定した、オプションで明示的に設定されるコンテキスト

  • 動的コンテキスト:実行中に変化を繰り返すことがあるコンテキスト

静的コンテキスト

次の方法で指定するコンテキストは静的コンテキストだ:

  • 実行コマンドでオプション --context=LABEL を指定する

  • 構成ファイルの [run] セクションでオプション context = LABEL を指定する

動的コンテキスト

構成ファイルでオプション dynamic_context を指定することで動的コンテキスト機能が効くようになる。現在のところ、実装されている動的コンテキストはただ一つあり、それは test_function だ。このように構成すると、名前が test から始まる関数それぞれが個別の動的コンテキストとして扱われ、網羅度測定データがそれぞれに分離される。

動的コンテキスト有効化構成
# myproject.toml
[tool.coverage.run]
dynamic_context = "test_function"  # or "none" to disable the feature

この機能を用いることにより、例えば「どのテストがこの行を実行したか」という問いに答えられるようになる。

コンテキストを指定して報告を見る方法

  • coverage report または coverage html の場合、オプション --contexts=REGEX,... を指定可能。正規表現パターンのいずれかに合致するコンテキストの測定データを報告する。

  • coverage html だけはオプション --show-contexts が使える。これをオンにすると、報告書の各網羅行にそのステップを実行したコンテキストの数を示す注釈が入る。その注釈をクリックすると関連コンテキスト一覧が表示される。

対象プロセスのサブプロセスを統制する方法

測定対象が Python サブプロセスを生成する場合、そのプロセスにおける網羅度測定には追加的手順が必要だ(メインプログラムは自然に測定対象となる)。簡単に言うと、サブプロセスそれぞれの開始時と終了時に Coverage.py に対して適切な合図をするのが不可欠だ。

開始時に関する方法は複数あり、コードに応じて採用しろ:

  • 構成でオプション patch = subprocess を設定する。これにより、作成した Python プロセスでデータを収集する設定が適用される。次のような方法で生成したプロセスで動作する:

    • 標準モジュール subprocess 機能

    • os.system()

    • os.execv() 系関数

    • os.spawnv() 系関数

  • 構成で次の二つを設定する。これにより、標準モジュール multiprocessing を使用して生成したプロセスでデータを収集する設定が適用される:

    • concurrency = multiprocessing

    • sigterm = true

関数 os.exec*e() および os.spawn*e() は新しいプログラムで使用する新しい環境変数を受け取るものだが、このために環境変数 COVERAGE_PROCESS_START の値を現在の環境から新しい環境にコピーするか、設定する必要がある。性質上、なるべく絶対パス形式で指定したい。

特に正常終了でないプロセス終了時に関する方法がある:

  • SIGTERM 受信によりプロセスが終了する場合でも測定データを書き込むようにする場合、オプション sigterm = true が要る。

  • プログラムが os._exit() を呼び出して終了する場合、オプション patch = _exit でパッチする。プロセス終了直前に測定データを書き出させる。

  • プログラムが os.execv() 系関数を呼び出してプロセスが置き換わるとき、これもオプション patch = execv でパッチする。こうすることで、測定データを置換直前に書き出させる。

  • サービスやデーモンなどの終了しない前提のプロセスに対しては、コマンド``coverage run`` 実行時にオプション --save-signal を与える。プロセスを終了させずに、測定データをファイルに書き込む信号をプロセスに送信することが任意の頃合いで可能だ。

Todo

完全手動方式。関数 coverage.process_startup() をコードから呼び出して網羅測定を実施する手順。

テストへの導入方法

プロジェクト管理に Hatch を利用するのが良い。フレームワークに Coverage.py が組み込まれていて、追加的構成を必要としないで網羅度測定をすぐに使える。

See also

Hatch 利用ノート に記したテストに関する記述

プラグイン

Coverage.py に対して、サードパーティー製プラグインを組み込むことでその動作を拡張可能だ。プラグインを登録するには構成ファイルを用いる。

  1. プラグインとなるパッケージを Python 環境にインストールする。

  2. 構成ファイルの [run] セクションでオプション plugins にプラグイン名を列挙する。

  3. さらに、プラグインが固有の構成を必要とする場合、構成ファイルの [coverage:PLUGIN_NAME] セクションでその固有構成を記述する。

後は、通常通りに Coverage.py を使用すればよい。プラグインの機能が動作するはずだ。

資料集