テストでos.Getwdがテストファイルのあるディレクトリを返す理由
先日、フューチャー技術ブログの記事を読んでいた際に、テストにおけるos.Getwd
の挙動についての以下の言及があり、普段何気なく書いていたテストコードでのこの挙動についてなぜそうなるのか気になったので実装を追いかけてみました。
GoにはC/C++の__FILE__だったり、Pythonの__file__、Node.jsの__dirname__に相当するものはないため、現在のソースコードのフォルダの場所を知ることはできません。できないはず、と思っていたのですがテストに関しては例外で、os.Getwd()でテストのソースがあるフォルダをワーキングディレクトリとして取得できます。実装依存で仕様にはない動作だと思うので、今後もこの結果が保証されるかはわかりませんが、まあ変わることもないかなと。
引用元: Goのテストでファイルの読み書きを扱いたい
テストにおけるワーキングディレクトリ
まず、Goのテストではワーキングディレクトリがパッケージディレクトリになっていることを前提としたコードが書かれることがあります。
例えば、Goのimage
パッケージを見ると、testdata
に置いたテスト用ファイルを参照する際に、相対パスで記述していますが、これは前述した挙動を前提としたコードと言えます。
そのため、os.Getwd
の実装の実装というよりはテスト実行(go test
)の仕組みを追いかけるのが良さそうです。
go testの実装を読む
まず、go test
の実装である golang/go/src/cmd/go/internal/test/test.go
を見てそれっぽい記述を探すと、cmd.Dir
に a.Package.Dir
(=テスト対象のパッケージディレクトリ)を代入している部分があります。
この設定をした後にcmd.Run()
を呼び出しており、周辺のコメント等から察するにここがテストバイナリを実行している箇所のようです。
cmd.Run()
の実装を追っていくと、os.StartProcess
が以下のように呼び出されています。
ここで、os.StartProcess
に渡されてる &os.ProcAttr{Dir: c.Dir, ...}
は、プロセスを起動する際の設定を行うためのもので、
このDir
にさきほど設定した cmd.Dir(= a.Package.Dir)
の値が代入されているため、結果としてテストバイナリはテスト対象パッケージのディレクトリをワーキングディレクトリとして起動されるようです。
os.StartProcessの実装を読む
go test
がテストバイナリを実行する際にワーキングディレクトリを設定していることはすでに読み取れましたが、せっかくなのでその設定をしているであろうシステムコールの呼び出し部分まで追ってみます。
os.StartProcess
の実装は以下のようになっており、この先は環境によって実装が切り替わるようになっています。
筆者は普段はAppleシリコン搭載のMacで開発しているため、この環境(GOOS=darwin, GOARCH=arm64
)でさらに実装を追っていきます。
まず、上記のstartProcess
はos/exec_posix.go
の実装が利用され、syscall.StartProcess
を呼び出しています。
このsyscall.StartProcess
はsyscall/exec_unix.go
の実装が利用されていました。
forkExec
を読んでいくとforkとexecを実際に呼び出していそうなforkAndExecInChild
の呼び出しが見つかります。
この関数も環境によって実装が切り替わるようで、syscall/exec_libc2.go
の実装が利用されていました。
forkAndExecInChild
ではrawSyscall
といういかにもな関数を呼んでおり、注意深く読み進めていくと、まずfork
を行って親と子に分岐させています。
その後、子プロセスで実行される箇所でchdir
を利用してワーキングディレクトリを指定されたディレクトリに変更しています。
最後にexecve
で指定したプログラム、つまりテストのバイナリに書き換えていました。
Unix系OSではプロセスを起動する際に利用するシステムコール(fork/exec系)にはワーキングディレクトリを指定するような仕組みはないので[1]、os.StartProcess
の内部実装ではfork → chdir → execとすることでワーキングディレクトリの変更を実現していることが確認できました。
まとめ
テストでos.Getwd
がテストファイルのあるディレクトリを返す挙動になっているのはgo test
の実装によるものでした。
go test
はテストバイナリのビルド → 実行 → 結果の取得と表示を行うコマンドです。この実行のステップではプロセスを立ち上げており、ここでテストファイルのあるパッケージディレクトリがこのプロセスのワーキングディレクトリとなるような操作をしているため、テストコード内で os.Getwd
を呼ぶと、そのパッケージディレクトリ(テストファイルのある場所)のパスが返ってくる仕組みになっていることが確認できました。
-
Windowsのプロセスを生成するAPI(CreateProcessAsUserW, CreateProcessW)はワーキングディレクトリを指定できる引数があり、これが利用されていました(syscall/exec_windows.go#L312-L318, syscall/exec_windows.go#L387-L391) ↩︎
Discussion