📁

テストでos.Getwdがテストファイルのあるディレクトリを返す理由

2025/02/18に公開

先日、フューチャー技術ブログの記事を読んでいた際に、テストにおけるos.Getwdの挙動についての以下の言及があり、普段何気なく書いていたテストコードでのこの挙動についてなぜそうなるのか気になったので実装を追いかけてみました。

GoにはC/C++の__FILE__だったり、Pythonの__file__、Node.jsの__dirname__に相当するものはないため、現在のソースコードのフォルダの場所を知ることはできません。できないはず、と思っていたのですがテストに関しては例外で、os.Getwd()でテストのソースがあるフォルダをワーキングディレクトリとして取得できます。実装依存で仕様にはない動作だと思うので、今後もこの結果が保証されるかはわかりませんが、まあ変わることもないかなと。
引用元: Goのテストでファイルの読み書きを扱いたい

テストにおけるワーキングディレクトリ

まず、Goのテストではワーキングディレクトリがパッケージディレクトリになっていることを前提としたコードが書かれることがあります。

例えば、Goのimageパッケージを見ると、testdataに置いたテスト用ファイルを参照する際に、相対パスで記述していますが、これは前述した挙動を前提としたコードと言えます。

https://github.com/golang/go/blob/go1.24.0/src/image/decode_test.go#L26-L52

そのため、os.Getwdの実装の実装というよりはテスト実行(go test)の仕組みを追いかけるのが良さそうです。

go testの実装を読む

まず、go testの実装である golang/go/src/cmd/go/internal/test/test.go を見てそれっぽい記述を探すと、cmd.Dira.Package.Dir(=テスト対象のパッケージディレクトリ)を代入している部分があります。

https://github.com/golang/go/blob/go1.24.0/src/cmd/go/internal/test/test.go#L1626-L1627

この設定をした後にcmd.Run()を呼び出しており、周辺のコメント等から察するにここがテストバイナリを実行している箇所のようです。

https://github.com/golang/go/blob/go1.24.0/src/cmd/go/internal/test/test.go#L1661

cmd.Run()の実装を追っていくと、os.StartProcess が以下のように呼び出されています。

https://github.com/golang/go/blob/go1.24.0/src/os/exec/exec.go#L725-L730

ここで、os.StartProcess に渡されてる &os.ProcAttr{Dir: c.Dir, ...} は、プロセスを起動する際の設定を行うためのもので、
https://github.com/golang/go/blob/go1.24.0/src/os/exec.go#L250-L255

このDirにさきほど設定した cmd.Dir(= a.Package.Dir)の値が代入されているため、結果としてテストバイナリはテスト対象パッケージのディレクトリをワーキングディレクトリとして起動されるようです。

os.StartProcessの実装を読む

go testがテストバイナリを実行する際にワーキングディレクトリを設定していることはすでに読み取れましたが、せっかくなのでその設定をしているであろうシステムコールの呼び出し部分まで追ってみます。

os.StartProcessの実装は以下のようになっており、この先は環境によって実装が切り替わるようになっています。
https://github.com/golang/go/blob/go1.24.0/src/os/exec.go#L304-L320

筆者は普段はAppleシリコン搭載のMacで開発しているため、この環境(GOOS=darwin, GOARCH=arm64)でさらに実装を追っていきます。

まず、上記のstartProcessos/exec_posix.goの実装が利用され、syscall.StartProcess を呼び出しています。
https://github.com/golang/go/blob/go1.24.0/src/os/exec_posix.go#L55

このsyscall.StartProcesssyscall/exec_unix.goの実装が利用されていました。
https://github.com/golang/go/blob/go1.24.0/src/syscall/exec_unix.go#L256-L260

forkExecを読んでいくとforkとexecを実際に呼び出していそうなforkAndExecInChildの呼び出しが見つかります。
https://github.com/golang/go/blob/go1.24.0/src/syscall/exec_unix.go#L207-L214

この関数も環境によって実装が切り替わるようで、syscall/exec_libc2.goの実装が利用されていました。
https://github.com/golang/go/blob/go1.24.0/src/syscall/exec_libc2.go#L44-L55

forkAndExecInChildではrawSyscallといういかにもな関数を呼んでおり、注意深く読み進めていくと、まずforkを行って親と子に分岐させています。
https://github.com/golang/go/blob/go1.24.0/src/syscall/exec_libc2.go#L83-L96

その後、子プロセスで実行される箇所でchdirを利用してワーキングディレクトリを指定されたディレクトリに変更しています。
https://github.com/golang/go/blob/go1.24.0/src/syscall/exec_libc2.go#L179-L185

最後にexecveで指定したプログラム、つまりテストのバイナリに書き換えていました。
https://github.com/golang/go/blob/go1.24.0/src/syscall/exec_libc2.go#L279-L283

Unix系OSではプロセスを起動する際に利用するシステムコール(fork/exec系)にはワーキングディレクトリを指定するような仕組みはないので[1]os.StartProcessの内部実装ではfork → chdir → execとすることでワーキングディレクトリの変更を実現していることが確認できました。

まとめ

テストでos.Getwdがテストファイルのあるディレクトリを返す挙動になっているのはgo testの実装によるものでした。

go testはテストバイナリのビルド → 実行 → 結果の取得と表示を行うコマンドです。この実行のステップではプロセスを立ち上げており、ここでテストファイルのあるパッケージディレクトリがこのプロセスのワーキングディレクトリとなるような操作をしているため、テストコード内で os.Getwdを呼ぶと、そのパッケージディレクトリ(テストファイルのある場所)のパスが返ってくる仕組みになっていることが確認できました。

脚注
  1. Windowsのプロセスを生成するAPI(CreateProcessAsUserW, CreateProcessW)はワーキングディレクトリを指定できる引数があり、これが利用されていました(syscall/exec_windows.go#L312-L318, syscall/exec_windows.go#L387-L391) ↩︎

Discussion