BioErrorLog Tech Blog

試行錯誤の記録

リソースファイルの相対パスがテストでズレる問題 | Go言語

embedを使えば簡単に解決する、という備忘録。

はじめに

フォントファイルなどのリソースファイルを雑に相対パスで指定したとき、go test実行時にパスがズレておりno such file or directoryエラーになってしまう問題に遭遇しました。

embedを使えば簡単に解決したので備忘録です。

リソースファイルの相対パスがテストでズレる問題: no such file or directory

問題

話をわかりやすくするため、問題を単純化/極端化して見ていきます。

.
├── main.go
└── resource
    ├── font.go
    ├── font_test.go
    └── myfont.ttf

例えばこのようなディレクトリ構成において、font.goにて下記のように雑に相対パスでttfフォントファイルを読み込むとします。

package resource

import (
    "log"
    "os"

    "golang.org/x/image/font"
    "golang.org/x/image/font/opentype"
)

var (
    FontFace font.Face
)

func init() {
    ttf, err := os.ReadFile("resouce/myfont.ttf")  // 相対パスでttfファイルを読み込む
    if err != nil {
        log.Fatalf("failed to read font file: %v", err)
    }
    tt, err := opentype.Parse(ttf)
    if err != nil {
        log.Fatalf("failed to parse font: %v", err)
    }
    const dpi = 72
    FontFace, err = opentype.NewFace(tt, &opentype.FaceOptions{
        Size:    50,
        DPI:     dpi,
        Hinting: font.HintingFull,
    })
    if err != nil {
        log.Fatalf("failed to create font face: %v", err)
    }
}

これは、プロジェクトrootから実行すると問題なくttfフォントファイルが読み込まれます。

しかし、go testによってテストコードを実行すると、この相対パスresouce/myfont.ttfで指定したttfフォントファイルは読み込まれず、no such file or directoryエラーになります。

failed to read font file: open resouce/myfont.ttf: no such file or directory

原因

プロジェクトrootから実行するときと、go testで実行されるときで、working directoryが異なることが原因です。

それぞれ、

  • プロジェクトrootから実行するとき: プロジェクトroot
  • go testで実行されるとき: テストファイルがあるディレクトリ

がworking directoryとなります。

※ これは下記のようにworking directoryを表示させれば簡単に確認できます。

func init() {
    cwd, err := os.Getwd()
    if err != nil {
        log.Fatalf("Error getting current working directory: %v", err)
    }
    log.Printf("Current working directory: %s", cwd)
}


よってテストファイルがネストしたディレクトリ構造配下にある場合は、相対パスで指定するとテスト実行時にズレが生じる、という訳です。

解決策

embedを使うことで、この問題は簡単に解決します。

embedは、外部ファイルを実行バイナリに埋め込むことができる機能です。

package font

import (
    _ "embed"
    "log"

    "golang.org/x/image/font"
    "golang.org/x/image/font/opentype"
)

var (
    //go:embed myfont.ttf
    fontData []byte
    fontFace font.Face
)

func init() {
    tt, err := opentype.Parse(fontData)
    if err != nil {
        log.Fatalf("failed to parse font: %v", err)
    }
    const dpi = 72
    fontFace, err = opentype.NewFace(tt, &opentype.FaceOptions{
        Size:    50,
        DPI:     dpi,
        Hinting: font.HintingFull,
    })
    if err != nil {
        log.Fatalf("failed to create font face: %v", err)
    }
}

ここで、↓の部分でmyfont.ttfファイルがfontData変数に埋め込まれるようになります。

   //go:embed myfont.ttf
    fontData []byte

//go:embedディレクティブに与えるリソースファイルのパスは、そのコードのある場所からの相対パスになります。 これはコード実行時もテスト実行時も変わらないので、先述したような問題は発生しません。

おわりに

相対パスがテストでズレる問題を、embedで回避する方法をメモしました。

Working directoryをいじるとか、相対パスから絶対パスに変換しておくとかもありそうですが、embedを使うとシンプルに書けそうなので良いですね。

Go言語は初学者なので、まだまだ知らないことがたくさんありそうです。

[関連記事]

www.bioerrorlog.work

参考

embed package - embed - Go Packages

Go by Example: Embed Directive

ebiten/examples/resources/fonts/embed.go at main · hajimehoshi/ebiten · GitHub