Scala のプログラムのインタラクティブなデバッグ方法あれこれ
Scala のプログラムのインタラクティブなデバッグ方法あれこれを紹介する.
次のようなパッケージを開発しているとしよう.
package com.example
object Lib {
def doSomething(): Int = 42
}
ThisBuild / organization := "com.example"
val lib = project.in(file("."))
.settings(
scalaVersion := "3.3.1",
version := "0.1.0-SNAPSHOT",
libraryDependencies ++= Seq(
"org.scalameta" %% "munit" % "1.0.0-M7" % Test
)
)
予習
- sbt shell: 通常のシェル(zsh、fish や bash)で sbt と入力したときに開始される sbt のインタラクティブなインターフェース.
- sbt console: sbt shell から呼び出せる Scala REPL 環境
以下の例ではコードブロックの行頭が sbt>
と書かれている場合 sbt shell 内でのコマンド実行を、scala>
と書かれている場合は Scala REPL 内でのコマンド実行を意味する.
sbt console
小さなプログラムであれば手っ取り早いのは sbt console(REPL) に入って実際にプログラムを動かしてみる方法.
build.sbt があるディレクトリで sbt console
コマンドを呼ぶ.
sbt console
scala>import com.example.Lib
scala>Lib.doSomething()
res0: Int = 42
scala>
複数行の入力
scala>:paste
// Entering paste mode (ctrl-D to finish)
object DeepThought {
val theAnswer = 42
}
// ctrl-D
ファイルの読み込み
// DeepThought.scala
object DeepThought {
val theAnswer = 42
}
scala>:load DeepThought.scala
sbt console でデバッグ: 初期化コマンドの設定
console/initialCommands
を設定すれば sbt console の初期化処理を追加できる.
毎回特定のモジュールやライブラリやインポートする場合は設定しておくと楽.
// build.sbt
val lib = project.in(file("."))
.settings(
scalaVersion := "3.3.1",
version := "0.1.0-SNAPSHOT",
console / initialCommands := "import java.nio.file._",
libraryDependencies ++= Seq(
"org.scalameta" %% "munit" % "1.0.0-M7" % Test
)
)
sbt console でデバッグ: サブプロジェクト
特定のサブプロジェクトのデバッグをしたい場合は sbt shell 内で projects
コマンドを実行しサブプロジェクトの名前を確認し projectname/console
とすればいい.
例えば core、contrib サブプロジェクトがあって、contrib サブプロジェクトのコードをインポートできる Scala REPL に入るには sbt contrib/console
コマンドを実行する.
├── build.sbt
├── contrib/src/main/scala/...
├── core/src/main/scala/...
└── project
lazy val core = project.in(file("core"))
...
lazy val contrib = project.in(file("contrib"))
...
sbt console でデバッグ: Pros & Cons
Pros
- 楽. セットアップや(ローカル)リリースが必要ない.
- 期待する動作がそこまで定まっていなくてもいい.
Cons
- パッケージやアプリケーション全体をデバッグするのには向かない.
- JS や Native ではデバッグできない.
- コンパイルが通っている必要がある(consoleQuick を使えば回避できる)
sbt test & sbt testOnly
src/test/scala 以下にテストを書いて動作確認する.
package com.example
class Test extends munit.FunSuite {
test("Lib.doSomething returns 42") {
assertEquals(Lib.doSomething, 42)
}
}
sbt shell でテストコマンドを打つことで実行できる.
sbt>test
test
コマンドは全てのテストを実行する.
testOnly
コマンドを使って特定のテストケースだけ実行することも可能.
sbt>testOnly com.example.Test
ScalaTest を利用している場合 -- -z <pattern>
オプションを指定することでパターンにマッチするテストだけ実行できる.
sbt>testOnly com.example.Test -- -z doSomething
testQuick
コマンドで失敗したテストケースだけ再実行することもできる.
sbt>testQuick
watch
~
をコマンドの前につけるとソースコードの変更を監視して、変更があれば自動でコマンドを再実行してくれる.
sbt>~testOnly com.example.Test
test でデバッグ: Pros & Cons
Pros
- JS や Native でも動作確認できる.
- console より IDE のサポートを活用できる.
Cons
- コンパイルを通さないといけない.
- テストを書かないといけない.
- ユニットテストが書きにくいコードベースだと辛い.
- ある程度期待する動作が定まっている必要がある.
sbt publishLocal
パッケージをローカルに配布して Scala CLI や ammonite からデバッグする.
publishLocal コマンドを実行すると、~/.ivy2/local/com.example/lib/0.1.0-SNAPSHOT
のようなローカルのファイルシステム上にパッケージを配布できる.
sbt
sbt>publishLocal
ローカルに配布した後は通常のパッケージのように sbt の libraryDependencies や Scala CLI の using dep
ディレクティブ、ammonite のマジックインポートで利用できる.
sbt project の場合
libraryDependencies ++= Seq(
"com.example::lib:0.1.0-SNAPSHOT"
)
Scala CLI の場合
//> using dep "com.example::lib:0.1.0-SNAPSHOT"
import com.example.Lib
@main
def run = println(Lib.doSomething())
scala-cli debug.scala
42
ammonite REPL の場合
@ import $ivy.`com.example::lib:0.1.0-SNAPSHOT`
@ import com.example.Lib
@ Lib.doSomething()
res0: Int = 42
@
Private Maven Repository に配布されたパッケージをデバッグ
Private Maven Repository にパッケージを配布&Private Maven Repository からパッケージを取得してローカルでデバッグする例
Google Artifact Registry に配布する場合
以下では GCP の asia-northeast1 の example-project プロジェクトに example という artifact registry がある想定で説明する. GitHub Packages なども同様のアイディアでできる.
sbt-gcs-resolver を使って GCP Artifact Registry にパッケージを配布する
addSbtPlugin("org.latestbit" % "sbt-gcs-plugin" % "1.8.0")
ThisBuild / organization := "com.example"
val publishSettings = Seq(
publishTo := Some(
"My Maven Artifact Registry" at "artifactregistry://asia-northeast1-maven.pkg.dev/example-project/example"
)
)
val lib = project.in(file("."))
.settings(
scalaVersion := "3.3.1",
version := "0.1.0-SNAPSHOT",
libraryDependencies ++= Seq(
"org.scalameta" %% "munit" % "1.0.0-M7" % Test
)
)
.settings(publishSettings)
自分のアカウントに artifact registry へのアクセス権限がある場合は、以下のようにしてデフォルト認証情報を設定すればプラグインがよしなに認証してくれる.
gcloud auth application-default login
sbt publish
サービスアカウントのファイルを指定する場合は以下のように設定する.
googleCredentialsFile := Some(new File("<your-account-file>"))
sbt project から Private パッケージの取得
addSbtPlugin("org.latestbit" % "sbt-gcs-plugin" % "1.8.0")
resolvers += "My Maven Artifact Registry" at "artifactregistry://asia-northeast1-maven.pkg.dev/example-project/example"
libraryDependencies ++= Seq(
"com.example::lib:0.1.0-SNAPSHOT"
)
Scala CLI から Private パッケージの取得
asia-northeast1 の example-project プロジェクトの example registry に接続する.
以下では、Google Artifact Registry へのアクセス権限があるサービスアカウント mypkg-user@example-project.iam.gserviceaccount.com
がある想定で説明する.
認証情報を設定する.
cat <service-account-credentials>.json| tr -d '\n' | base64
scala-cli --power config repositories.credentials "asia-northeast1-maven.pkg.dev" value:_json_key_base64 value:eyXXInR5cXXiOiXXc2.....
設定を間違えた場合は以下のコマンドでリセットできる.
scala-cli --power config --remove repositories.credentials
認証情報をハードコードしたくない場合は value:eyXXInR5cXXiOiXXc2.....
の部分を env:<ENV_VAR_KEY>
に変える. こうすると環境変数 <ENV_VAR_KEY>
からよしなに読み取ってくれる.
scala-cli --power config repositories.credentials "asia-northeast1-maven.pkg.dev" value:_json_key_base64 env:MVN_PKG_CREDENTIAL
export MVN_PKG_CREDENTIAL=eyXXInR5cXXiOiXXc2.....
これで https://asia-northeast1-maven.pkg.dev/examle-project/example からパッケージを取得できるようになる.
//> using repository "https://asia-northeast1-maven.pkg.dev/examle-project/example"
//> using dep "com.example::lib:0.1.0-SNAPSHOT"
import com.example._
@main
def run =
println(Lib.doSomething())
Discussion