$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

【Scala】Emacsで現在編集している部分のテストを実行する

Scalaのテストを実行する時、sbtを使うと、特定のテストクラスだけの実行や、特定のテストケースだけの実行ができる。やり方はScalatest: 特定のテストケースだけ実行したい - Qiita で紹介されているとおり。

ただ、コードはテキストエディタで書いているので、このコマンドを使うとしても

  • 編集を終える
  • 今のファイルのテストクラス名とdescribeの文字列をコピーする
  • ターミナルを開いて、sbtでtestOnlyを使ってコピーした文字列を貼り付けて実行

のように、結構面倒という問題がある。

そこでEmacsで現在編集している部分のテストを実行するユーティリティを作り、編集 -> テスト -> 編集のループを回しやすくしてみた。今回はそのことについてご紹介。

今回のユーティリティで出来ること

以下のように、現在ファイルのテストクラスだけEmacs上で実行したり

さらに、現在のカーソルが存在するdescribeのテストだけ実行したりできる。以下のアニメーションではScalaTestExample#listだけ実行されている。

実現方法

現在編集中のバッファから、パッケージ名・テストクラス名・describeを抜き出し、それをsbt-modeに渡すことで実現できる。

sbt-mode というものを使うと、Emacs上でsbtを動かし、それに対してコマンドを送ることができる。まずこれをインストールする。

M-x package-install RET sbt-mode

後は以下のような実装を行うと、sbt/test-only-current-specで現在のバッファのテストクラスを実行でき、sbt/test-only-current-describeで現在カーソルがいるdescribeのテストだけを実行できる。

;;; 現在のバッファのテストクラスを実行する
(defun sbt/test-only-current-spec ()
  "Run test with current file."
  (interactive)
  (sbt-command
   (format "testOnly %s" (scala/find-spec-name-with-package-current-buffer))))

;;; 現在カーソルがいるdescribeのテストだけを実行する
(defun sbt/test-only-current-describe ()
  "Run current describe test"
  (interactive)
  (sbt-command
   (format "testOnly %s -- -z \"%s\""
           (scala/find-spec-name-with-package-current-buffer)
           (scala/find-nearest-spec-describe-current-buffer))))

;;; 現在のバッファからpackage名とテストクラス名を結合したものを返す
(defun scala/find-spec-name-with-package-current-buffer ()
  "Find spec name with package in current buffer."
  (interactive)
  (let* ((package-name (scala/find-package-name-current-buffer))
         (spec-name (scala/find-spec-name-current-buffer)))
    (if (string= package-name "")
        spec-name
      (format "%s.%s" package-name spec-name))))

;;; 現在のバッファからpackage名を抜き出す
(defun scala/find-package-name-current-buffer ()
  "Find package name in current buffer"
  (interactive)
  (let* ((matched-package ""))
    (save-excursion
      (when (re-search-backward "^package \\(.+\\)$" nil t)
        (setq matched-package (match-string 1))))
    matched-package))

;;; 現在のバッファからテストクラス名を抜き出す
(defun scala/find-spec-name-current-buffer ()
  "Find spec name of current buffer."
  (interactive)
  (let* ((matched-spec-name ""))
    (save-excursion
      (when (re-search-backward "^class \\([^ ]+Spec\\) " nil t)
        (setq matched-spec-name (match-string 1))))
    matched-spec-name))

;;; 現在カーソルがいるdescribe名を抜き出す
(defun scala/find-nearest-spec-describe-current-buffer ()
  (interactive)
  (let* ((matched-describe-name ""))
    (save-excursion
      (when (re-search-backward "\\bdescribe(\"\\([^\"]+\\\)\")" nil t)
        (setq matched-describe-name (match-string 1))))
    matched-describe-name))

後は好きなキーバインドを割り当てる。

(define-key scala-mode-map (kbd "C-c C-t") 'sbt/test-only-current-spec)
(define-key scala-mode-map (kbd "C-c t") 'sbt/test-only-current-describe)

また、僕は popwin で表示するのが好きなので、以下のようにsbt-modeのバッファはポップアップするように設定した。

(push '("\*sbt\*" :regexp t :height 0.5 :stick t) popwin:special-display-config)

技術的Tips

今回の実装は、「現在バッファの特定の文字列を抜き出す」ということが出来れば簡単に実装できる。やり方はこんな感じ。

(save-excursion
  (when (re-search-backward "^class \\([^ ]+Spec\\) " nil t)
    (setq matched-spec-name (match-string 1))))

まず、save-excursionでバッファの状態を保存しておく。これがないと文字列を探索した時にカーソルの位置が変わってしまう。

次にre-search-backwardを使って、現在カーソルより上の文字列を正規表現で探索する。抜き出したい部分を括弧を使って取り出せるようにしておく。

後はmatch-stringを使って、括弧で囲っていた部分を取り出す。1番目の括弧の中身を取り出したいので、1を指定している。

まとめ

今回はEmacsで現在編集している部分のScalaのテストを実行する方法について書いてみた。これによってテストクラス名とかをコピペする手間が省けるようになって便利。