VSCodeでRustのテストコードをデバッグできない問題

2018エディションにNLLも入って久しいし、tonic(async/await構文でgRPCサービスを書けるライブラリ) からベータが取れたぞということで久しぶりにRustやろうという気持ちが高まってきました(2年ぶり3回目)。

まずは開発環境を更新せねばということで諸々アップデートして動作を確認していたのですが、 相変わらずVSCode + LLDBという構成だとlibクレートの単体テストをデバッグするときにブレークポイントが効かない。binクレートの単体テストだと効く。 なぜか日本語のブログではこの問題に触れているのを見たことがないんですよね (そういう入門記事はだいたいサンプルがbinクレートなので、この問題を踏むのはもっと後のステップではあるんですが)。

私の環境固有の問題かなとも少し思ったんですが、GitHubのIssueを漁ってみると同じ問題を踏んでそうな人をちらほら見かけたので、 今回はちょっと時間を使って原因を調べてみることにしました。

問題の再現手順とかワークアラウンドのスクリプトは下記のリポジトリにも置いてあります。

https://github.com/kwhrtsk/rust-lldb-workaround

解決方法だけ知りたい人はVSCode (CodeLLDB) の場合を参照してください。

問題の概要

macOSにおいて、rust-lldb でlibクレートのテストをデバッグしようとしたときに、ブレークポイントが効きません。 VSCodeで CodeLLDB を使ってデバッグした場合も同様です。

【2020-03-06:追記】

masterには修正が入りました。詳細は末尾の追記を参照してください。

原因

直接の原因は、cargo test--libオプションを付けてテストバイナリをビルドすると、 LLDBのデバッグシンボル情報が書かれたファイル(.dSYM)がtarget/debug/ディレクトリに作成されないことです。

このため、ブレークポイントとして指定したシンボル情報をLLDBが解決できず、デバッガがブレークポイントで止まりません。

rust-lldbを使った場合は、後述のようにエラーが表示されるのですが、 VSCode上でデバッグしている場合、lldb.verboseLoggingを有効にして出力ペインを注意深く見ていないと気づけないと思います。

環境

  • Operating system: macOS Catalina 10.15.3
  • Rust toolchain: stable-x86_64-apple-darwin
  • Rustc version: 1.41.1 (f3e1a954d 2020-02-24)
  • lldb: 9.0.1
  • VSCode: 1.42.1
  • CodeLLDB: 1.5.0

再現手順

  • Cargo.toml
[package]
name = "rust-lldb-workaround"
version = "0.1.0"
edition = "2018"
  • src/lib.rs
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

it_worksにブレークポイントを仕掛けようとすると、下記のように失敗します。

下記のようなシェルスクリプトを実行するとこの様子を確認できます。

#!/bin/bash

rm -rf target
bin=$(cargo test --lib --no-run --message-format=json | jq -r '.executable')
rust-lldb $bin <<- EOF
breakpoint set --name tests::it_works
EOF
   Compiling rust-lldb-workaround v0.1.0 (/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround)
    Finished test [unoptimized + debuginfo] target(s) in 0.51s
(lldb) command script import "/Users/kawahara_taisuke/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/etc/lldb_rust_formatters.py"
(lldb) type summary add --no-value --python-function lldb_rust_formatters.print_val -x ".*" --category Rust
(lldb) type category enable Rust
(lldb) target create "/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-6e0ca18365abb7b9"
Current executable set to '/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-6e0ca18365abb7b9' (x86_64).
(lldb) breakpoint set --name tests::it_works
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.

詳細

binクレートの場合、cargo test.dSYMファイルをtarget/debug/ディレクトリに作成します。 このファイルは実際にはtarget/debug/deps/以下のファイルへのシンボリックリンクです。

% cargo test --bins --no-run --message-format=json 2> /dev/null | jq 'select(.target.kind | contains(["bin"])) | .filenames'
[
  "/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-4aae58342b9c3866",
  "/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-4aae58342b9c3866.dSYM"
]

% ls -l target/debug
total 1720
drwxr-xr-x  2 kawahara_taisuke  staff      64  3  3 13:47 build
drwxr-xr-x  8 kawahara_taisuke  staff     256  3  3 13:47 deps
drwxr-xr-x  2 kawahara_taisuke  staff      64  3  3 13:47 examples
drwxr-xr-x  4 kawahara_taisuke  staff     128  3  3 13:47 incremental
-rwxr-xr-x  2 kawahara_taisuke  staff  874952  3  3 13:47 rust_lldb_workaround-4aae58342b9c3866
-rw-r--r--  1 kawahara_taisuke  staff     282  3  3 13:47 rust_lldb_workaround-4aae58342b9c3866.d
lrwxr-xr-x  1 kawahara_taisuke  staff      47  3  3 13:47 rust_lldb_workaround-4aae58342b9c3866.dSYM -> deps/rust_lldb_workaround-4aae58342b9c3866.dSYM

しかし、libクレートを対象とするとこのシンボリックリンクが作成されません。 rust-lldb及びVSCodeのデバッガ(CodeLLDB)でブレークポイントが効かないのは直接的にはこれが原因です。

% cargo test --lib --no-run --message-format=json 2> /dev/null | jq 'select(.target.kind | contains(["lib"])) | .filenames'
[
  "/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-6e0ca18365abb7b9"
]

% ls -l target/debug
total 1720
drwxr-xr-x  2 kawahara_taisuke  staff      64  3  3 13:49 build
drwxr-xr-x  5 kawahara_taisuke  staff     160  3  3 13:49 deps
drwxr-xr-x  2 kawahara_taisuke  staff      64  3  3 13:49 examples
drwxr-xr-x  3 kawahara_taisuke  staff      96  3  3 13:49 incremental
-rwxr-xr-x  2 kawahara_taisuke  staff  874968  3  3 13:49 rust_lldb_workaround-6e0ca18365abb7b9
-rw-r--r--  1 kawahara_taisuke  staff     201  3  3 13:49 rust_lldb_workaround-6e0ca18365abb7b9.d

% ls -l target/debug/deps
total 1720
-rwxr-xr-x  2 kawahara_taisuke  staff  874968  3  3 13:49 rust_lldb_workaround-6e0ca18365abb7b9
-rw-r--r--  1 kawahara_taisuke  staff     290  3  3 13:49 rust_lldb_workaround-6e0ca18365abb7b9.d
drwxr-xr-x  3 kawahara_taisuke  staff      96  3  3 13:49 rust_lldb_workaround-6e0ca18365abb7b9.dSYM

cargo の実装を見る限りこの挙動は意図したもので、テストの場合はdepsの下を直接実行してもらう想定だったようです。(参考)

https://github.com/rust-lang/cargo/blob/e618d47a1765ca18d1601d4cf891a55a34d23aed/src/cargo/core/compiler/build_context/target_info.rs#L260-L269

対処法としては次の方法が考えられます。

  1. rust-lldbadd-dsymコマンドを実行する。
  2. .dSYMへのリンクをtarget/debug/以下に作る。
  3. cargo test --message-format=jsonの出力するexecutabledepsの下に変更する。

1の方法はCodeLLDBなど使用するVSCode拡張全てに対処が必要で、あまり筋がよくないと思ったので除外しました。

2と3は根本的に解決しようとすると両方ともcargoの動作を変える必要があるのですが、 2についてはcargo testrust-lldbの間にシンボリックリンクを作る処理を挟めば良いので簡単そうです。

この記事では2の方法でのワークアラウンドを示します。

rust-lldbコマンドを直接実行する場合

先ほどのシェルスクリプトを下記のように修正します。

#!/bin/bash

rm -rf target
bin=$(cargo test --lib --no-run --message-format=json | jq -r '.executable')

# .dSYMディレクトリへのシンボリックリンクを作成
(cd target/debug && for d in deps/*.dSYM; do ln -sf $d ./; done)

rust-lldb $bin <<- EOF
breakpoint set --name tests::it_works
EOF

ブレークポイントの設置が成功するようになります。

   Compiling rust-lldb-workaround v0.1.0 (/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround)
    Finished test [unoptimized + debuginfo] target(s) in 0.53s
(lldb) command script import "/Users/kawahara_taisuke/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/etc/lldb_rust_formatters.py"
(lldb) type summary add --no-value --python-function lldb_rust_formatters.print_val -x ".*" --category Rust
(lldb) type category enable Rust
(lldb) target create "/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-6e0ca18365abb7b9"
Current executable set to '/Users/kawahara_taisuke/.ghq/github.com/kwhrtsk/rust-lldb-workaround/target/debug/rust_lldb_workaround-6e0ca18365abb7b9' (x86_64).
(lldb) breakpoint set --name tests::it_works
Breakpoint 1: where = rust_lldb_workaround-6e0ca18365abb7b9`rust_lldb_workaround::tests::it_works::h666f078a6b384dfd + 18 at lib.rs:7:8, address = 0x0000000100000cd2

VSCode (CodeLLDB) の場合

launch.jsontasks.json を修正して、cargo testの直後に.dSYMのシンボリックリンクを自動で作成するようにします。

  • .vscode/launch.json

preLaunchTaskを追加した以外はVSCodeからLLVMテンプレートで作ったものそのままです。

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "lldb",
      "request": "launch",
      "name": "Debug unit tests in library 'rust-lldb-workaround'",
      "cargo": {
        "args": [
          "test",
          "--no-run",
          "--lib",
          "--package=rust-lldb-workaround"
        ],
        "filter": {
          "name": "rust-lldb-workaround",
          "kind": "lib"
        }
      },
      "args": [],
      "cwd": "${workspaceFolder}",
      // この行を追加
      "preLaunchTask": "symlink dSYM"
    }
  ]
}
  • .vscode/tasks.json

launch.jsonpreLaunchTaskで参照されているタスクの定義です。 このタスクでは、target/debugディレクトリに移動してtarget/debug/deps/ディレクトリ以下の全ての*.dSYMのシンボリックリンクを作成します。

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "symlink dSYM",
      "type": "shell",
      "command": "sh",
      "args": [
        "-c",
        "cd ${workspaceFolder}/target/debug; for d in deps/*.dSYM; do ln -fs $d ./; done"
      ]
    }
  ]
}

なお Rust Test Lens を使う場合は、上記のタスクを自動で実行することができません。 この場合は事前に一度 cargo test --lib --no-run を実行した後、手動で上記のタスクを実行して .dSYMのシンボリックリンクを作っておく必要があります。

終わりに

この問題はrust-lang/cargo#7960で既に起票されているので、将来的には修正されるかもしれません。

【2020-03-06:追記】

rust-lang/cargo#7965 で修正され、masterにもマージされました。
この修正は2020年4月23日リリース予定の Cargo 1.43 に反映される見込みです。