Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 17135] ファイルの概要

このコミットは、Go言語のEmacsメジャーモードであるgo-mode.elに、go tool coverコマンドが生成するカバレッジレポートのmode=count形式のサポートを追加するものです。これにより、Emacs内でGoコードのカバレッジ強度を視覚的に表現できるようになります。具体的には、コードの実行回数に応じて異なる色を適用し、カバレッジの深さを一目で把握できるように改善されています。

コミット

commit 4a0d06c4c5f457e650ab816b76d4754e7f6cd34c
Author: Dominik Honnef <dominik.honnef@gmail.com>
Date:   Fri Aug 9 14:42:43 2013 -0700

    misc/emacs: add support for mode=count coverage
    
    Use the same algorithm that go tool cover uses when producing HTML
    output to render coverage intensity.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/12712043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/4a0d06c4c5f457e650ab816b76d4754e7f6cd34c

元コミット内容

misc/emacs/go-mode.el ファイルが変更され、78行が追加され、19行が削除されました。主な変更点は以下の通りです。

  • カバレッジ強度を示すための新しいEmacsフェイス(go-coverage-0からgo-coverage-10)が定義されました。
  • go--covered構造体にcountフィールドが追加され、コードブロックの実行回数を保持できるようになりました。
  • go-coverage関数が、mode=count形式のカバレッジデータを解析し、実行回数に基づいてコードに色付きのオーバーレイを適用するように修正されました。
  • go tool coverのHTML出力が使用するのと同じ対数スケールアルゴリズムを適用し、カバレッジ強度を視覚的に表現します。
  • オーバーレイにhelp-echoプロパティが追加され、マウスカーソルを合わせたときに実行回数を表示するようになりました。

変更の背景

Go言語には、go test -coverprofileコマンドによって生成されるコードカバレッジプロファイルと、それをHTMLレポートとして視覚化するgo tool coverコマンドがあります。go tool coverは、単にコードが実行されたかどうか(mode=set)だけでなく、各コードブロックが何回実行されたか(mode=count)という情報も提供できます。

このコミット以前のEmacsのgo-mode.elは、おそらくmode=setのカバレッジ情報(実行されたか否か)のみを視覚化する機能しか持っていなかったと考えられます。しかし、開発者にとっては、テストがコードのどの部分をどれだけ深くテストしているかを理解するために、実行回数に基づくカバレッジ強度(mode=count)の情報が非常に有用です。

この変更の背景には、Emacsユーザーがgo tool coverが提供する詳細なカバレッジ情報を、Emacsの統合開発環境内で直接利用できるようにするというニーズがありました。これにより、開発者はテストの網羅性をより詳細に分析し、テストが不足している領域や、特定のコードパスが頻繁に実行されている領域を特定しやすくなります。

前提知識の解説

Go言語のテストカバレッジ

Go言語の標準ツールチェーンには、コードカバレッジを測定する機能が組み込まれています。

  • go test -coverprofile=coverage.out: このコマンドは、テスト実行中にどのコードが実行されたかの情報を収集し、coverage.outというファイルにプロファイルとして出力します。
  • go tool cover: このコマンドは、coverage.outファイルの内容を解析し、様々な形式でカバレッジレポートを生成します。
    • go tool cover -html=coverage.out: 最も一般的に使用される形式で、カバレッジ情報を色分けされたHTMLページとして出力します。このHTMLレポートでは、実行されたコードは緑色、実行されなかったコードは赤色で表示されます。
    • カバレッジモード: go testは、カバレッジ情報を収集する際にいくつかのモードをサポートしています。
      • mode=set: 各ステートメントが実行されたかどうか(真偽値)のみを記録します。これはデフォルトのモードです。
      • mode=count: 各ステートメントが何回実行されたかを記録します。このモードは、テストが特定のコードパスをどれだけ頻繁に通過するかを理解するのに役立ちます。
      • mode=atomic: mode=countに似ていますが、並行処理環境での正確なカウントのためにアトミック操作を使用します。

Emacs Lisp (Elisp) の基本概念

EmacsはEmacs Lisp(Elisp)というLisp方言で拡張可能です。このコミットで使われているElispの主要な概念は以下の通りです。

  • defface: Emacsの表示属性(色、フォント、太字など)のセットを定義するために使用されます。定義されたフェイスは、テキストの特定の領域に適用して視覚的なスタイルを変更できます。
  • defvar: グローバル変数を定義します。
  • defstruct: 構造体(他のプログラミング言語の構造体やクラスに似たデータ構造)を定義します。これにより、関連するデータをまとめて扱うことができます。
  • defun: 関数を定義します。
  • interactive: 関数を対話的に呼び出せるようにします(例: M-x function-name)。
  • with-temp-buffer: 一時的なバッファを作成し、その中でコードを実行します。コードの実行後、一時バッファは自動的に削除されます。
  • insert-file-contents: 指定されたファイルの内容を現在のバッファに挿入します。
  • re-search-forward: 正規表現を使って前方検索を行います。
  • string-to-number: 文字列を数値に変換します。
  • file-name-nondirectory: ファイルパスからディレクトリ部分を除いたファイル名のみを抽出します。
  • make-overlay: テキストの特定の領域に「オーバーレイ」を作成します。オーバーレイは、テキスト自体を変更せずに、その表示方法(色、フォントなど)や動作(クリック時の挙動、ツールチップなど)を変更するために使用されます。
  • overlay-put: 作成したオーバーレイにプロパティ(例: facehelp-echo)を設定します。
  • let*: 変数を順番に束縛するためのマクロです。前の束縛が後の束縛で利用できます。
  • cond: 複数の条件分岐を記述するための特殊形式です。
  • log: 自然対数を計算する関数です。
  • floor: 数値を最も近い小さい整数に切り捨てる関数です。
  • concat: 複数の文字列を結合する関数です。
  • number-to-string: 数値を文字列に変換する関数です。
  • format: フォーマット文字列と引数を使って新しい文字列を生成します。

対数スケールによるカバレッジ強度の表現

go tool coverのHTML出力では、コードの実行回数が多いほど色が濃くなるようなグラデーションが使われます。この際、実行回数をそのまま線形に色にマッピングすると、非常に大きな実行回数を持つブロックが少数あるだけで、他の多くのブロックの色がほとんど変わらなくなってしまいます。

これを解決するために、対数スケールが用いられます。対数スケールは、大きな数値の範囲をより小さな、管理しやすい範囲に圧縮するのに役立ちます。例えば、実行回数が1回、10回、100回、1000回といったように桁が異なる場合でも、対数を取るとそれぞれ0, 1, 2, 3といった線形に近い値になり、色のグラデーションに均等にマッピングしやすくなります。

このコミットでは、go tool coverがHTML出力で利用しているのと同じ対数スケールアルゴリズムをEmacs Lispで実装し、カバレッジ強度を視覚的に表現しています。

技術的詳細

このコミットの核となる技術的詳細は、go tool coverのHTML出力がカバレッジ強度を視覚化するために使用するアルゴリズムをEmacs Lispで再現している点にあります。

  1. カバレッジデータの解析: go-coverage関数は、go test -coverprofileによって生成されたカバレッジプロファイルファイル(例: coverage.out)を読み込みます。このファイルは、各コードブロックの開始/終了位置と、そのブロックの実行回数(count)を含む行指向のフォーマットを持っています。

    file:start_line.start_column,end_line.end_column count
    

    例: main.go:10.5,12.10 3 (main.goの10行目5列から12行目10列のブロックが3回実行された)

  2. 最大実行回数 (max-count) の特定: 解析中に、すべてのコードブロックの中で最も高い実行回数(max-count)を特定します。これは、カバレッジ強度の正規化の基準となります。

  3. 対数スケールによる正規化: go tool coverのアルゴリズムと同様に、実行回数countを対数スケールで正規化します。

    • divisor = (log max-count): max-countの自然対数を計算し、これを正規化のための除数とします。
    • norm = (/ (log count) divisor): 各コードブロックのcountの自然対数をdivisorで割ることで、countを0から1の範囲(おおよそ)に正規化します。
      • count=1の場合、log(1)=0なのでnormは0に近くなります。
      • count=max-countの場合、log(max-count)/log(max-count)=1なのでnormは1になります。
  4. 色インデックスへのマッピング: 正規化されたnorm値は、0から9の範囲の整数nにマッピングされます。 n = (1+ (floor (* norm 9)))

    • (* norm 9): normを0から9の範囲にスケーリングします。
    • floor: 小数点以下を切り捨てて整数にします。
    • 1+: 結果を1から10の範囲にシフトします。 このnは、go-coverage-1からgo-coverage-10までのフェイス名の一部として使用されます。
  5. 特殊ケースのハンドリング:

    • count = 0の場合: norm-0.1に設定されます。これは、go-coverage-0フェイス(通常は赤色で未カバーを示す)を適用するために使用されます。
    • max-count = 1の場合: これはmode=setのカバレッジに相当します(実行されたか否かのみ)。この場合、norm0.8に設定されます。これにより、n1 + floor(0.8 * 9) = 1 + floor(7.2) = 1 + 7 = 8となり、go-coverage-8フェイスが適用されます。go-coverage-8は、以前のgo-coverage-coveredと同じ色(緑色)に設定されており、後方互換性を保ちつつ、mode=setのカバレッジも適切に表示できるようにしています。
  6. Emacsオーバーレイの適用: 計算されたnに基づいて、go-coverage-Nという名前のフェイスが動的に構築され、対応するコードブロックにオーバーレイとして適用されます。これにより、コードの実行回数に応じた色のグラデーションがEmacsバッファに表示されます。 さらに、overlay-putを使ってhelp-echoプロパティが設定されます。これにより、ユーザーが色付けされたコードにマウスカーソルを合わせると、「Count: X」(Xは実際の実行回数)というツールチップが表示され、詳細な情報を提供します。

この一連の処理により、go tool coverのHTMLレポートと同様の視覚的なカバレッジ強度表現がEmacs内で実現されます。

コアとなるコードの変更箇所

変更はすべて misc/emacs/go-mode.el ファイル内で行われています。

  1. 新しいフェイスの定義:

    (defface go-coverage-0
      '((t (:foreground "#c00000")))
      "Coverage color for uncovered code."
      :group 'go-cover)
    ;; ... go-coverage-1 to go-coverage-10 ...
    (defface go-coverage-10
      '((t (:foreground "#14ec9b")))
      "Coverage color for covered code with weight 10."
      :group 'go-cover)
    

    既存の go-coverage-uncovered は削除され、go-coverage-0 がその役割を引き継ぎます。

  2. go--covered 構造体の変更:

    --- a/misc/emacs/go-mode.el
    +++ b/misc/emacs/go-mode.el
    @@ -960,7 +999,7 @@ description at POINT."
         (point))))
     
     (defstruct go--covered
    -  start-line start-column end-line end-column covered)
    +  start-line start-column end-line end-column covered count)
    

    count フィールドが追加されました。

  3. go-coverage 関数の変更:

    • 新しい変数 max-countdivisor の導入。
    • カバレッジプロファイルの解析中に max-count を計算するロジックの追加。
    • make-go--covered 呼び出しで count フィールドをセット。
    • オーバーレイを適用するメインのループ内で、countmax-countdivisor を使って normn を計算し、動的にフェイス名を生成するロジックの追加。
    • overlay-puthelp-echo プロパティを設定。
    --- a/misc/emacs/go-mode.el
    +++ b/misc/emacs/go-mode.el
    @@ -968,7 +1007,9 @@ coverage information gathered via go test -coverprofile=INPUT."
       (interactive "fCoverage file: ")
       (let ((ranges '())
             (file-name (file-name-nondirectory (buffer-file-name)))
    -        (gocov-buffer-name (concat (buffer-name) "<gocov>"))))
    +        (gocov-buffer-name (concat (buffer-name) "<gocov>"))
    +        (max-count 0)
    +        divisor))
     
         (with-temp-buffer
           (insert-file-contents input)
    @@ -982,6 +1023,9 @@ coverage information gathered via go test -coverprofile=INPUT."
                   (start-line start-column end-line end-column num count)
                   (mapcar 'string-to-number rest)
     
    +            (if (> count max-count)
    +                (setq max-count count))
    +
                 (if (and (string= (file-name-nondirectory file) file-name))
                     (push
                      (make-go--covered
    @@ -989,10 +1033,14 @@ coverage information gathered via go test -coverprofile=INPUT."
                       :start-column start-column
                       :end-line end-line
                       :end-column end-column
    -                  :covered (/= count 0))\n+                  :covered (/= count 0)
    +                  :count count)\n                   ranges))))
     
    -          (forward-line))))
    +          (forward-line))))
    +
    +      (if (> max-count 0)
    +          (setq divisor (log max-count))))
     
         (with-current-buffer (or
                               (get-buffer gocov-buffer-name)
    @@ -1006,15 +1054,26 @@ coverage information gathered via go test -coverprofile=INPUT."
              'face 'go-coverage-untracked)
     
             (dolist (range ranges)
    -          (overlay-put
    -           (make-overlay
    -            (go--line-column-to-point
    -             (go--covered-start-line range)
    -             (go--covered-start-column range))\n-            (go--line-column-to-point
    -             (go--covered-end-line range)
    -             (go--covered-end-column range)))
    -           'face (if (go--covered-covered range) 'go-coverage-covered 'go-coverage-uncovered))))
    +          (let* ((count (go--covered-count range))
    +                 (norm (cond
    +                        ((= count 0)
    +                         -0.1)
    +                        ((= max-count 1)
    +                         0.8)
    +                        (t
    +                         (/ (log count) divisor))))
    +                 (n (1+ (floor (* norm 9))))
    +                 (face (concat "go-coverage-" (number-to-string n)))
    +                 (ov (make-overlay
    +                      (go--line-column-to-point
    +                       (go--covered-start-line range)
    +                       (go--covered-start-column range))
    +                      (go--line-column-to-point
    +                       (go--covered-end-line range)
    +                       (go--covered-end-column range)))))\n+
    +            (overlay-put ov 'face face)
    +            (overlay-put ov 'help-echo (format "Count: %d" count)))))\n     
           (display-buffer (current-buffer) 'display-buffer-reuse-window))))
    

コアとなるコードの解説

このコミットの最も重要な部分は、go-coverage 関数内の dolist ループで、各カバレッジ範囲に対してオーバーレイを適用するロジックです。

            (dolist (range ranges)
              (let* ((count (go--covered-count range))
                     (norm (cond
                            ((= count 0)
                             -0.1)
                            ((= max-count 1)
                             0.8)
                            (t
                             (/ (log count) divisor))))
                     (n (1+ (floor (* norm 9))))
                     (face (concat "go-coverage-" (number-to-string n)))
                     (ov (make-overlay
                          (go--line-column-to-point
                           (go--covered-start-line range)
                           (go--covered-start-column range))
                          (go--line-column-to-point
                           (go--covered-end-line range)
                           (go--covered-end-column range)))))\n+
                (overlay-put ov 'face face)
                (overlay-put ov 'help-echo (format "Count: %d" count)))))\n
  1. count の取得: count (go--covered-count range): 現在処理しているカバレッジ範囲 (range) から、そのコードブロックの実行回数 (count) を取得します。

  2. norm の計算: norm は、count を正規化し、0から1の範囲に収めるための値です。

    • ((= count 0) -0.1): もし count が0(未実行)であれば、norm-0.1 に設定されます。これは後で go-coverage-0 フェイスにマッピングされます。
    • ((= max-count 1) 0.8): もし max-count が1であれば、これはmode=setのカバレッジ(実行されたか否かのみ)を意味します。この場合、norm0.8 に設定されます。これにより、n8 になり、go-coverage-8 フェイスが適用されます。このフェイスは、以前の「カバーされたコード」の色と同じです。
    • (t (/ (log count) divisor)): 上記のどちらでもない場合(つまり、count > 0 かつ max-count > 1)、count の自然対数を divisor で割ることで norm を計算します。divisor(log max-count) であり、これにより normlog_max_count(count) となり、0から1の範囲に正規化されます。
  3. n の計算: n (1+ (floor (* norm 9))): norm を0から9の範囲にスケーリングし、小数点以下を切り捨て、さらに1を加えることで、1から10の整数値 n を生成します。この n が、どの go-coverage-N フェイスを使用するかを決定します。

  4. face 名の生成: face (concat "go-coverage-" (number-to-string n)): n の値を使って、"go-coverage-0""go-coverage-1"、...、"go-coverage-10" といったフェイス名を動的に生成します。

  5. オーバーレイの作成とプロパティの設定:

    • ov (make-overlay ...): 現在のカバレッジ範囲に対応するテキスト領域に新しいオーバーレイを作成します。go--line-column-to-point は、行と列の情報をEmacsバッファ内のポイント(文字位置)に変換するヘルパー関数です。
    • (overlay-put ov 'face face): 生成されたフェイス名 (face) をオーバーレイに適用し、コードの色を変更します。
    • (overlay-put ov 'help-echo (format "Count: %d" count)): オーバーレイに help-echo プロパティを設定します。これにより、ユーザーがこのオーバーレイされたコードにマウスカーソルを合わせると、「Count: X」(Xは実際の実行回数)というツールチップが表示されます。

このロジックにより、go tool coverのHTML出力と同様に、コードの実行回数に応じたグラデーションカラーと詳細な実行回数情報がEmacs上で視覚的に提供されるようになります。

関連リンク

参考にした情報源リンク