テストで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