Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

go testのキャッシュの仕組みにDeep Diveする

go testのキャッシュの仕組みにDeep Diveする

Avatar for KazukiHayase

KazukiHayase

December 01, 2025
Tweet

More Decks by KazukiHayase

Other Decks in Technology

Transcript

  1. 自己紹介 @KazukiHayase 早瀬和輝(Kazuki Hayase) 2025年4月ドクターズプライム入社 リードエンジニア Go / TypeScript /

    React(Next.js) / GraphQL 葬送のフリーレン2期がとても楽しみです ©2025 Dr.'s Prime ,Inc.
  2. ソースコードの解説 1. 前提条件のチェック go test のように引数なしでは無効 func (c *runCache) tryCacheWithID(b

    *work.Builder, a *work.Action, id string) bool { if len(pkgArgs) == 0 { // Caching does not apply to "go test", // only to "go test foo" (including "go test ."). if cache.DebugTest { fmt.Fprintf(os.Stderr, "testcache: caching disabled in local directory mode\n") } c.disableCache = true return false } // ...省略 } ©2025 Dr.'s Prime ,Inc.
  3. ソースコードの解説 1. 前提条件のチェック モジュール、 GOPATH 、 GOROOT の配下にある必要がある if a.Package.Root

    == "" { // Caching does not apply to tests outside of any module, GOPATH, or GOROOT. if cache.DebugTest { fmt.Fprintf(os.Stderr, "testcache ...\n", a.Package.ImportPath) } c.disableCache = true return false } ©2025 Dr.'s Prime ,Inc.
  4. ソースコードの解説 2. テスト引数の検証 var cacheArgs []string for _, arg :=

    range testArgs { i := strings.Index(arg, "=") switch arg[:i] { case "-test.benchtime", "-test.cpu", "-test.list", "-test.parallel", "-test.run", "-test.short", "-test.skip", "-test.timeout", "-test.failfast", "-test.v", "-test.fullpath": cacheArgs = append(cacheArgs, arg) default: c.disableCache = true return false } } ©2025 Dr.'s Prime ,Inc.
  5. ソースコードの解説 2. テスト引数の検証 キャッシュ不可な引数が1つでもあれば無効化 run 、 timeout などはキャッシュ可能 coverprofile 、

    outputdir は値が変わっても無効化しない それ以外の引数はキャッシュ不可 ©2025 Dr.'s Prime ,Inc.
  6. ソースコードの解説 3. testIDの計算 h := cache.NewHash("testResult") // テストバイナリとテスト引数からハッシュ値を計算 fmt.Fprintf(h, "test

    binary %s args %q execcmd %q", id, cacheArgs, work.ExecCmd) testID := h.Sum() if c.id1 == (cache.ActionID{}) { c.id1 = testID } else { c.id2 = testID } ©2025 Dr.'s Prime ,Inc.
  7. ソースコードの解説 3. testIDの計算 計算した testID を元にテストログを取得 // Load list of

    referenced environment variables and files // from last run of testID, and compute hash of that content. data, entry, err := cache.GetBytes(cache.Default(), testID) if !bytes.HasPrefix(data, testlogMagic) || data[len(data)-1] != '\n' { if cache.DebugTest { if err != nil { fmt.Fprintf(os.Stderr, "testcache: %s: input list not found: %v\n", a.Package.ImportPath, err) } else { fmt.Fprintf(os.Stderr, "testcache: %s: input list malformed\n", a.Package.ImportPath) } } return false } ©2025 Dr.'s Prime ,Inc.
  8. ソースコードの解説 4. testInputsIDの計算 func computeTestInputsID(a *work.Action, testlog []byte) (cache.ActionID, error)

    { testlog = bytes.TrimPrefix(testlog, testlogMagic) h := cache.NewHash("testInputs") // The runtime always looks at GODEBUG, without telling us in the testlog. fmt.Fprintf(h, "env GODEBUG %x\n", hashGetenv("GODEBUG")) pwd := a.Package.Dir for _, line := range bytes.Split(testlog, []byte("\n")) { if len(line) == 0 { continue } s := string(line) op, name, found := strings.Cut(s, " ") if !found { return cache.ActionID{}, errBadTestInputs } // テストログの各行を処理、次のスライドで解説 } sum := h.Sum() return sum, nil } ©2025 Dr.'s Prime ,Inc.
  9. ソースコードの解説 4. testInputsIDの計算 switch op { case "getenv": fmt.Fprintf(h, "env

    %s %x\n", name, hashGetenv(name)) case "chdir": pwd = name // always absolute fmt.Fprintf(h, "chdir %s %x\n", name, hashStat(name)) case "stat": if !filepath.IsAbs(name) { name = filepath.Join(pwd, name) } if a.Package.Root == "" || search.InDir(name, a.Package.Root) == "" { // Do not recheck files outside the module, GOPATH, or GOROOT root. break } fmt.Fprintf(h, "stat %s %x\n", name, hashStat(name)) case "open": if !filepath.IsAbs(name) { name = filepath.Join(pwd, name) } if a.Package.Root == "" || search.InDir(name, a.Package.Root) == "" { // Do not recheck files outside the module, GOPATH, or GOROOT root. break } fh, err := hashOpen(name) if err != nil { return cache.ActionID{}, err } fmt.Fprintf(h, "open %s %x\n", name, fh) } ©2025 Dr.'s Prime ,Inc.
  10. ソースコードの解説 4. testInputsIDの計算 環境変数は存在しない場合は 0 、存在する場合は 1 +値をハッシュ化 func hashGetenv(name

    string) cache.ActionID { h := cache.NewHash("getenv") v, ok := os.LookupEnv(name) if !ok { h.Write([]byte{0}) } else { h.Write([]byte{1}) h.Write([]byte(v)) } return h.Sum() } ©2025 Dr.'s Prime ,Inc.
  11. ソースコードの解説 4. testInputsIDの計算 ファイルのハッシュ計算 func hashOpen(name string) (cache.ActionID, error) {

    h := cache.NewHash("open") info, err := os.Stat(name) if err != nil { fmt.Fprintf(h, "err %v\n", err) return h.Sum(), nil } hashWriteStat(h, info) if info.IsDir() { // ...省略 } else if info.Mode().IsRegular() { if time.Since(info.ModTime()) < modTimeCutoff { return cache.ActionID{}, errFileTooNew } } return h.Sum(), nil } func hashWriteStat(h io.Writer, info fs.FileInfo) { fmt.Fprintf(h, "stat %d %x %v %v\n", info.Size(), uint64(info.Mode()), info.ModTime(), info.IsDir()) } ©2025 Dr.'s Prime ,Inc.
  12. ソースコードの解説 5. テスト結果の取得 testID と testInputsID を組み合わせた最終キーで結果を取得 キャッシュの有効期限が切れている場合は無効化 // Parse

    cached result in preparation for changing run time to "(cached)". // If we can't parse the cached result, don't use it. data, entry, err = cache.GetBytes(cache.Default(), testAndInputKey(testID, testInputsID)) if entry.Time.Before(testCacheExpire) { return false } ©2025 Dr.'s Prime ,Inc.
  13. ソースコードの解説 5. テスト結果の取得 キャッシュがヒットした場合、実行時間を (cached) に書き換えて出力 // Committed to printing.

    c.buf = new(bytes.Buffer) c.buf.Write(data[:j]) c.buf.WriteString("(cached)") for j < len(data) && ('0' <= data[j] && data[j] <= '9' || data[j] == '.' || data[j] == 's') { j++ } c.buf.Write(data[j:]) return true ©2025 Dr.'s Prime ,Inc.
  14. CIでの活用 条件付きキャッシュクリア name: Clean Go Cache Conditionally runs: using: "composite"

    steps: - name: Check non go file changes id: changes uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 with: predicate-quantifier: 'every' filters: | has-not-go-file: - '!**/*.go' - '!**/*.mod' - '!**/*.sum' - name: Clean test cache if needed shell: bash run: | if [ "${{ steps.changes.outputs.has-not-go-file }}" == "true" ] || [ "${{ github.ref }}" == "refs/heads/main" ]; then echo "Cleaning test cache due to non-go file changes or main branch" go clean -testcache else echo "Skipping cache clean - only go files changed and not on main branch" fi ©2025 Dr.'s Prime ,Inc.
  15. CIでの活用 条件付きキャッシュクリア jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955

    # v4.3.0 - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version-file: 'go.mod' - name: Clean cache conditionally uses: ./.github/actions/clean-go-cache - name: Run tests run: go test -v ./... ©2025 Dr.'s Prime ,Inc.