Slide 1

Slide 1 text

OpCode目線で眺める PHPコードのカバレッジ PHPerKaigi 2024の補足 Hideki Kinjyo GitHub: o0h / X: @o0h_

Slide 2

Slide 2 text

これはなに? • PHPerKaigi 2024での発表 「phpunit/php-code-coverageって何をしてるんだ」の補足資料です • https://fortee.jp/phperkaigi-2024/proposal/8543c51c-7e06-45d5-a216- cba29cb29789 • OpCodeの内容とブランチ・パスカバレッジの対応を見ていきます • 発表内では端折った部分になります • でも調べていて面白かったので

Slide 3

Slide 3 text

php-code-coverageによる ブランチカバレッジとパスカバレッジ

Slide 4

Slide 4 text

Branch Coverage • 分岐(ブランチ)を考慮し 通ったコードの割合 • 同一行にある分岐も 区別できる • 右のスニペットは、 カバレッジが50%程度となる

Slide 5

Slide 5 text

$aの値に関わらず、必ず$aは評価される ($a=true || $b=true)は$aを無視しない ($a=false || $b=true)も$aを無視しない $aの値によって、$bは評価を省略される ($a=true || $b=true)は$bを通らない ($a=false || $b=true)は$bを通る

Slide 6

Slide 6 text

1つのコードに対して、 「場合分け」をした上での カバー範囲を判定している様子

Slide 7

Slide 7 text

「行」と「ブランチ」でカバレッジが異なる例 5行目は実行されている(カバーされている)が、 $aの短絡評価の結果により、$bの評価まで処理が行っていない(カバーされていない)

Slide 8

Slide 8 text

Path Coverage • 経路(=分岐の組み合わせ)で 通ったコードの割合 • 右のスニペットは、 カバレッジが1/8になる • boolの変数3つの組み合わせで 2^3=8通りのパスが存在 • テストできているのは1パス

Slide 9

Slide 9 text

ブランチの観点から カバレッジを満たしても パスではカバレッジが低いまま

Slide 10

Slide 10 text

カバレッジデータの取得 • `xdebug_start_coverage()`のコール時にフラグを立てる • なお、pcovではブランチカバレッジデータの取得に対応していないので注意 • その他については、ラインカバレッジ取得時の利用方法と差異なし XDEBUG_CC_BRANCH_CHECK ブランチごとの実行状態を収集する 単独では利用できず、 他のフラグと合わせて指定する

Slide 11

Slide 11 text

出力データの様子

Slide 12

Slide 12 text

全体の骨格 対象ファイルごとに同じkeyにまとまる

Slide 13

Slide 13 text

全体の骨格 ファイルごとのkeyになるのは コレまでと同じ `lines` `functions` に分かれる `lines` は ラインカバレッジのデータ

Slide 14

Slide 14 text

全体の骨格 ブランチカバレッジの データはココ

Slide 15

Slide 15 text

このコードは 3つのブランチからなると解釈される

Slide 16

Slide 16 text

3つのブランチ

Slide 17

Slide 17 text

ブランチとOpCode

Slide 18

Slide 18 text

OpCodeについて知る

Slide 19

Slide 19 text

OpCode? • PHPで書かれたコードが、CPUに渡される前の中間生成物 • PHPのソースコードを分解して、 単純化された命令の組み合わせに変換する • see: アセンブリ、仮想機械 • 代入・加算といった単純な処理や、ジャンプ命令から成る コレを辿れば ブランチが識別できる!

Slide 20

Slide 20 text

PHPコード ステップの単純化・分解 1. 引数$xに値をセットする 2. 引数$yに値をセットする 3. $xをチェックして、 真ならJUMP→⑤へ 4. $yをチェックする 5. おしまい!

Slide 21

Slide 21 text

PHPコード 実際のOpCode

Slide 22

Slide 22 text

1. 引数$xに値をセットする 2. 引数$yに値をセットする 3. $xをチェックして、真ならJUMP→⑤へ 4. $yをチェックする 5. おしまい!

Slide 23

Slide 23 text

ブランチについて知る

Slide 24

Slide 24 text

ブランチってなんだっけ • ブランチ = 分岐 • コードを上から処理して行って、 「共通して通る箇所」「条件によって分岐するところ」で分ける • 分けて出来上がったのがブランチ

Slide 25

Slide 25 text

このコードで言えば・・ 1. 引数$xに値をセットする 2. 引数$yに値をセットする 3. $xをチェックして、 真ならJUMP→⑤へ 4. $yをチェックする 5. おしまい!

Slide 26

Slide 26 text

このコードで言えば・・ 1. 引数$xに値をセットする 2. 引数$yに値をセットする 3. $xをチェックして、もし falseyでなければJUMP 4. $yをチェックする ① 引数$xに値をセットする ② 引数$yに値をセットする ③ $xをチェックして、 真ならJUMP→⑤へ ④ $yをチェックする ⑤ おしまい! ブランチその1

Slide 27

Slide 27 text

このコードで言えば・・ 1. 引数$xに値をセットする 2. 引数$yに値をセットする 3. $xをチェックして、もし falseyでなければJUMP 4. $yをチェックする ① 引数$xに値をセットする ② 引数$yに値をセットする ③ $xをチェックして、 真ならJUMP→⑤へ ④ $yをチェックする ⑤ おしまい! ブランチその2

Slide 28

Slide 28 text

このコードで言えば・・ 1. 引数$xに値をセットする 2. 引数$yに値をセットする 3. $xをチェックして、もし falseyでなければJUMP 4. $yをチェックする ① 引数$xに値をセットする ② 引数$yに値をセットする ③ $xをチェックして、 真ならJUMP→⑤へ ④ $yをチェックする ⑤ おしまい! ブランチその3

Slide 29

Slide 29 text

Xdebugの分析とブランチ

Slide 30

Slide 30 text

Xdebugによるブランチの分析 • 上のPHPコードを分析にかけると、 3つのブランチがあると分かる(先述の通り) • そのうち1つ目のブランチは 下のキャプの通りとなる

Slide 31

Slide 31 text

ブランチのデータの中身 op_start: ブランチの開始位置 op_end: ブランチの終了位置

Slide 32

Slide 32 text

ブランチのデータの中身 op_startの値は、branchesの各添字に対応する

Slide 33

Slide 33 text

ブランチのデータの中身 line_start: ブランチの開始行 line_end: ブランチの終了行 ※ PHPのソースコード上の番号

Slide 34

Slide 34 text

ブランチのデータの中身 JMPNZ_EX CV0(=$x)がNZ(not zero)のEX(でない)なら 0005にJMP(jump)する そうでなければ、 そのまま次の命令(0006)に進む out: ブランチの次に進む位置

Slide 35

Slide 35 text

ブランチごとの到達(実行)回数 hit: ブランチに到達した回数

Slide 36

Slide 36 text

未到達の場合も記録されている ブランチに到達しなかった例

Slide 37

Slide 37 text

ブランチごとの到達(実行)回数 out_hit: 出口に到達した回数

Slide 38

Slide 38 text

ブランチごとの到達(実行)回数 • outの0番目 • op_start:4のブランチ ($x == falseの時に到達する) • コッチはhitが0 • outの1番目 • op_start:5のブランチ • コッチはhitが1

Slide 39

Slide 39 text

ブランチのデータの中身 PHPソース、ブランチ、OpCodeの全景。(※ 2147483645は終端を示す番号)

Slide 40

Slide 40 text

Xdebugの分析とパスとOpCode

Slide 41

Slide 41 text

パス? • すべての可能性を組み合わせればパス(経路)を列挙できるので、 • ブランチのデータが取得できているなら、 それらを組み合わせれば良いことになる • Xdebugでの分析データには `out`も付いていたので、これを辿っていけばOK

Slide 42

Slide 42 text

XDEBUG_CC_BRANCH_CHECKでの分析データの 全体の骨格

Slide 43

Slide 43 text

XDEBUG_CC_BRANCH_CHECKでの分析データの 全体の骨格 今から注目するのはココ

Slide 44

Slide 44 text

このコードは 20のパスからなると解釈される

Slide 45

Slide 45 text

通るブランチの番号 (=op_start)の列挙

Slide 46

Slide 46 text

ブランチとパスのデータ(一部、フォーマット済み) 0を起点に、全てのoutを辿っていけば全パスを網羅できる

Slide 47

Slide 47 text

hit: パスに到達した回数

Slide 48

Slide 48 text

おまけ: ブランチカバレッジのレポートについて

Slide 49

Slide 49 text

こんなコードがあったとします

Slide 50

Slide 50 text

L8に注目してください。 $xと$yそれぞれの分岐に対応し、2ブランチ分のデータが出ていますよね

Slide 51

Slide 51 text

a2()については、L13が3回出てきました。 一見すると、不思議に感じませんか?

Slide 52

Slide 52 text

OpCodeを見てみると、 `return`の差分があることが分かります(`0005`)

Slide 53

Slide 53 text

PHPUnitによるブランチの分析を見てみます ※ ग़ྗ͞ΕͨཁૉΛҰ෦࡟আ͍ͯ͠·͢

Slide 54

Slide 54 text

op_start/endが異なる2つのブランチで、 line_startが重複しています

Slide 55

Slide 55 text

中身を見るとこんな様子

Slide 56

Slide 56 text

可視化すると、こういう状態になります

Slide 57

Slide 57 text

まとめ

Slide 58

Slide 58 text

まとめ • 「行よりも詳しい単位で、どうやってコードを解釈するの?」 を見てみました • 個人的に、ココを調べてみよう!!となったキッカケは大きく2つです • ブランチカバレッジって、どんな仕組みで処理されているんだ?気になる • レポートに同じ行が重複出てるけど、何で?気になる • 仕組みを知ることで、 「なんだシンプル!なるほど!!」を得られちゃった・・・楽しい! • 皆さんも身近な所にある沼を探して、半身浴を楽しんでみてはいかが?

Slide 59

Slide 59 text

Appendix

Slide 60

Slide 60 text

参考資料 個人的にもとってもお世話に資料たち! • Xdebug: Documentation » Code Coverage Analysis https://xdebug.org/docs/code_coverage • PHPerのための「Xdebugの活用方法」を語るTechCafe https://hackmd.io/X0Qv5k5oSYm-tMU9Vw6r3A?view • xdebug-new-phpsouthcoast17 https://derickrethans.nl/talks/xdebug-new-phpsouthcoast17.pdf

Slide 61

Slide 61 text

参考資料 個人的にもとってもお世話に資料たち! • プログラムを実行するとはどういうことか https://speakerdeck.com/tomzoh/what-does-it-mean-to-execute-a-program • PHPのOpcodeを 読んでみよう https://speakerdeck.com/yasuaki640/phpnoopcodewo-du-ndemiyou • opcodeダンプするのにvldもphpdbgも要らなくなってた #PHP https://qiita.com/hnw/items/352c5030d6729343a49e • How to dump and inspect PHP OPCodes • PHP.Watch https://php.watch/articles/php-dump-opcodes

Slide 62

Slide 62 text

おしまい! お付き合いいただき ありがとうございました!!