Upgrade to Pro — share decks privately, control downloads, hide ads and more …

時の止め方を考える

 時の止め方を考える

Go Connect#3で発表した資料です。
https://gotalk.connpass.com/event/331992/

tenntenn - Takuya Ueda

October 23, 2024
Tweet

More Decks by tenntenn - Takuya Ueda

Other Decks in Technology

Transcript

  1. The Go gopher was designed by Renée French. The gopher

    stickers was made by Takuya Ueda. Licensed under the Creative Commons 3.0 Attributions license. 時の止め方を考える 2024/10/22 Go Connect #3 https://tenn.in/ctxtime
  2. UEDA Takuya / tenntenn 上田拓 也 newmo株式会社 / ソフトウェアエンジニア 一般社団法人

    Gophers Japan 代表理事 Google Developers Expert (GDE) / Go Category tenntenn Conference 主催・登壇者 2013年よりGo Conferenceの運営を行う。 
 2016年、メルカリグループに入社、Goコミュニティへの貢献や採用・社内教育などに従事。 
 2021年、一般社団法人Gophers Japan設立、代表理事に就任。Google Developers Expert (GDE)に選出。 
 2022年、株式会社ナレッジワーク入社。ソフトウェアエンジニアおよびGoエンジニアのイネーブルメントに従事。 
 2024年、newmo株式会社入社。ソフトウェアエンジニアとしてプロダクト開発に従事。 
 略歴
  3. 時を止めるとは? ▪ 時刻を使ったコードのテストがしたい • 特定の時刻で固定する package greet func Greet() string

    { now := time.Now() switch h := now.Hour() { case h >= 4 && h <= 9: return "おはよう" case h >= 10 && h <= 16: return "こんにちは" default: return "こんばんは" } }
  4. やんちゃな時の止め方 - testtime func Now() $GOROOT/src/time/time.go testtime 静的解析 func Now()

    $GOPATH/testtime/xxxx/time.go 書き換え go test -overlay 利用 参考:https://tenntenn.dev/ja/posts/2021-07-06-testtime/
  5. 静的解析による time.Now関数さがし ▪ GOROOTを取得する • go env GOROOTコマンドを実行 • runtime.GOROOT関数ではコンパイル時のGOROOTになりダメ

    ▪ パッケージの情報を取得 • (*build.Context).Importメソッドで取得できる • Context.GOROOTフィールドを変更しておく必要がある • パッケージがあるディレクトリのパスを取得 ▪ timeパッケージを構文解析する • parser.ParseDir関数でパースしてAST(抽象構文木を取得) ▪ ソースコードを変更する • ASTを変更する • x/tools/go/astutil.AddImport関数をimport宣言を追加する • go/format.Node関数で出力する
  6. -overlayオプション ▪ ビルド時にファイルを変更する • Overlay JSONのパスを指定する • パスを読み替えることができる ▪ testtimeの実行結果を指定する

    • 作成したOverlay JSONのパスをstdoutに表示する • go test -overlay $(testtime)で入れ替えることができる { "Replace": { "/usr/local/go/src/time/time.go": "/Users/tenntenn/go/pkg/testtime/time_go1.16.go" } }
  7. //go:linkname ▪ リンカでシンボルを読み替える • 外部パッケージのシンボル(識別子)を参照できる • Go1.23からstdへのlinknameは禁止になった ◦ ハンドシェイクしてあればOK ▪

    自作自演でハンドシェイク • -overlayで上書きするのでハンドシェイクを加える ▪ 共通のsync.Mapを置く • timeパッケージに宣言を追加 • testtimeパッケージがlinknameしておく
  8. ゴルーチンの ID ▪ スタックトレースを自力でパースして取得する • いつ変更されるか分からないので真似してはダメ func goroutineID() string {

    var buf [64]byte n := runtime.Stack(buf[:], false) // 10: len("goroutine ") for i := 10; i < n; i++ { if buf[i] == ' ' { return string(buf[10:i]) } } return "" }
  9. まじめな時の止め方 - ctxtime ▪ コンテキストに現在時刻を設定する • テストの時だけNow関数を入れ替える ctxtime.Now internal.Now ctxtimetest

    呼び出し internal.DefaultNow 呼び出し 入れ替え ctxtimetest.nowForTest 呼び出し 参考:https://tech.newmo.me/entry/2024/09/20/133402
  10. テストのときだけ挙動を変える ▪ internalを使った3pkg方式 • ctxtimeパッケージはtestingパッケージに依存したくない • internalパッケージに実装を置いておき変更可能にしておく ◦ パッケージ関数ではなく変数にしておく •

    ctxtimetestパッケージをimportするとinit関数で入れ替える ◦ testing.Testing関数でテストか判定できる ctxtime.Now internal.Now ctxtimetest 呼び出し internal.DefaultNow 呼び出し 入れ替え ctxtimetest.nowForTest 呼び出し
  11. testid ▪ テストに個別のIDをふる • https://github.com/newmo-oss/testid • (*testing.T).Nameメソッドでテスト名は取得できる ◦ 子テストも含めて •

    テスト関数より多くIDが振りたくなることを見越して作成 ▪ コンテキストに仕込む • ミドルウェアやインタセプターで仕込む • ログやらいろんなところでテストが識別できて便利
  12. Linterなきルールはルールではない ▪ time.Now関数を呼び出している箇所を見つける func run(pass *analysis.Pass) (any, error) { in

    := pass.ResultOf[ssainspect.Analyzer].(*ssainspect.Inspector) timenow, _ := analysisutil.ObjectOf(pass, "time", "Now").(*types.Func) if timenow == nil { return nil, nil } for in.Next() { c := in.Cursor() if analysisutil.Called(c.Instr, nil, timenow) { pass.Reportf(c.Instr.Pos(), "do not use %s, use ctxtime.Now", timenow.FullName()) } } return nil, nil }