Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Gremlins はどのようにコードを変異させているのか 👾
Search
Radish
July 22, 2026
Programming
52
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Gremlins はどのようにコードを変異させているのか 👾
社内勉強会
Radish
July 22, 2026
More Decks by Radish
See All by Radish
論文紹介: SKILLHARM
radish
0
85
Miyazaki.go 勉強会 #4
radish
0
58
Other Decks in Programming
See All in Programming
ハーネス設計入門 〜 基礎知識の整理から実務へのステップアップ 〜
kinopeee
28
33k
Performance Optimization With LLMs
xleitix
0
140
Apple Intelligence を用いた個人情報誤送信防止、及びユーザーリクエスト体験の改善について
yukiny
0
350
Herb in Rails 8.2: Your ERB views, now HTML-aware @ Kaigi on Rails 2026, Tokyo, Japan
marcoroth
2
280
KiroのSpecで「五目並べ」を作ってみる
satoshi256kbyte
1
380
機械に任せるテスト、人が見るテスト ⽣成AIで引き直した、運⽤保守フェーズの線引き
dske104
0
190
エージェントを使い続けるためのメタワーク / Meta-work for keeping using the agent
rkaga
1
190
気持ちぃ〜角丸 Squircle
ryu1013
0
320
[JA] Domain Modeling with Vanilla Rails: 「操作」から再考するRailsの設計論
yasaichi
3
440
コードで差別化できない時代の ソフトウェアエンジニアリング / Software Engineering in an Era Where Code Doesn’t Differentiate
kishida
2
180
HTML-in-Canvasで作る未来のWeb UI表現
clockmaker
5
5.2k
contenteditable と日本語入力に向き合う
colorful12
0
200
Featured
See All Featured
VelocityConf: Rendering Performance Case Studies
addyosmani
331
25k
The #1 spot is gone: here's how to win anyway
tamaranovitovic
4
1.2k
[Rails World 2026] Durable orchestration on Rails: from continuation to workflow
palkan
1
750
A Soul's Torment
seathinner
8
3.8k
GraphQLとの向き合い方2022年版
quramy
50
15k
GraphQLの誤解/rethinking-graphql
sonatard
75
12k
Git: the NoSQL Database
bkeepers
PRO
433
67k
Writing Fast Ruby
sferik
630
63k
Noah Learner - AI + Me: how we built a GSC Bulk Export data pipeline
techseoconnect
PRO
0
460
sira's awesome portfolio website redesign presentation
elsirapls
0
460
Code Reviewing Like a Champion
maltzj
529
40k
Done Done
chrislema
187
17k
Transcript
Gremlins はどのようにコードを 変異させているのか👾 社内勉強会
ミューテーションテストとは 本番コードに小さな変更(変異体 / mutant)を機械的に埋め込み、その状態でテストを走らせる手法。 ⚰️ killed(殺された) 👾 lived / survived(生き残った)
カバレッジと何が違う? スコア(Test efficacy) 変異体を入れたらテストが落ちた → そのバグは テストで検出できている ✅ カバレッジ:その行をテストが通ったかしか分 からない ミューテーションテスト:その行のバグを検出 できるかまで測れる 変異体を入れてもテストが通ってしまう → 検出で きない穴がある(偽陰性)🔴 killed ÷(killed + lived) 変異体をどれだけ殺せたか=テストスイートの検出力
Gremlins とは Go 向けのミューテーションテスト CLI 対象パッケージを解析し、変異体を自動で作って テストを回す Java の定番ツール PITest
に触発されて開発 想定は小さめのモジュール(マイクロサービスな ど)。大きいと実行に時間がかかる gremlins unleash Mutation testing completed in 1m30s Killed: 10, Lived: 2, Not covered: 3 Timed out: 0, Not viable: 1, Skipped: 0 Test efficacy: 83.33% Mutator coverage: 80.00% 実行結果の例(この形式で集計とスコアが出る) デフォルトで有効な変異体 👾 変異体 変異の例 CONDITIONALS_BOUNDARY >= → > CONDITIONALS_NEGATION == → != ARITHMETIC_BASE + → INCREMENT_DECREMENT ++ → -INVERT_NEGATIVES -a → +a 変異体は全11種。残りはフラグで有効化する
では、どうやってコードを 書き換えているの?🤔
気になりませんか?🤔
気になります!👾
全体パイプライン( gremlins unleash の中身) 準備は1回だけ。そのあと mutant を1個ずつ並列ループで処理する。 準備フェーズ(1回だけ) 1 作業ディレクトリを用意
2 カバレッジを取得 3 ソースをAST にパース 4 変異ポイントを列挙 実行フェーズ(変異体ごと・並列ループ) 5 worker の作業コピーを確保(初回のみ) 6 本番コードを差し替える 7 go test で killed / lived 判定 8 元に戻す(Rollback) 実装: cmd/unleash.go run() → (*Engine).Run() 変異ポイントを全部見つけてから処理するのではなく、見つけたそばから worker に渡して処理する (=列挙 3–4 と実行 5–8 が同時進行)
準備フェーズ
① 作業ディレクトリを用意する Gremlins は本番のソースツリーを直接は書き換えない。 一時ディレクトリを作成し、その中だけで変異させる。 一時ディレクトリを1つ作る 起動時に gremlins-* を os.TempDir()
に 1個作成 変異・テストはすべてこの中で完結 実行後は丸ごと削除( defer で後始末) workDir, _ := os.MkdirTemp(os.TempDir(), "gremlins-" defer cleanUp(workDir) 実装: cmd/unleash.go ( MkdirTemp / cleanUp ) なぜコピーで作業するのか 失敗・中断しても本番ソースは無傷 この領域の中に、後でworker ごとのコピーを 作る(→ ⑤)
② カバレッジを取得する 変異させる前に、素の状態で go test を1回だけ走らせて情報を集める。 ここで測る2つのこと どの行がテストで実行されるか (カバレッジ) テストの実行にかかる実時間(Elapsed)
go test -cover -coverprofile=<f> ./... 後段でどう使うか カバーされた行の mutantだけをテスト対象に する → 通らない行は最初から NOT COVERED でス キップ 実時間 × 係数(既定 5)をテストのタイムア ウトに 実装: internal/coverage/coverage.go ( Run() が Profile と Elapsed を持つ Result を返す)
③ ソースを AST にする ファイルを1ファイルずつパース ( _test.go は除外) 使うのは Go
標準の go/parser / go/ast / go/token だけ .go set := token.NewFileSet() file, _ := parser.ParseFile( set, fileName, src, parser.ParseComments) 型チェックをしない go/types も packages.Load も使っていな い 意味解析なしの純粋なトークン操作 だからシンプルで速い。反面、型を見た賢い 変異はしない ast.Inspect(file, func(n ast.Node) bool { // 変異できるノードを探す return true }) AST はファイルごとに1回だけ生成し、実行フェーズでは同じ木を全 worker が共有する(コピーしない) 実装: internal/engine/engine.go ( runOnFile )
④ 変異ポイントを見つける 対象は演算子・代入など5種のノードだけ。共通点は 「トークンを1個持つ」こと。 case *ast.BinaryExpr: tok = &n.Op pos
= n.OpPos AssignStmt UnaryExpr // a >= b の >= // ← トークンへのポインタ / BinaryExpr / BranchStmt / IncDecStmt / ポインタで保持しているので、後から書き換えられる func (n *NodeToken) SetTok(t token.Token) { *n.tok = t } 「元トークン → 変異後」の対応表 変異体 例 ConditionalsBoundary >= → > , < → <= ConditionalsNegation == → != , > → <= ArithmeticBase +→-, *→/ InvertLogical && → || InvertLoopCtrl break → continue 実装: internal/engine/mappings.go の TokenMutantType / tokenMutations
実行フェーズ
⑤ worker ごとに作業コピーを確保する ループの各回は、まず自分用の作業コピーを受け取る。worker が初めて担当したときに1度だけ作られる。 worker ごとに丸ごとコピー worker 数 =
並列度(既定 CPU 数) ルート gremlins-* の中に worker ごとの wd-* を作り、対象モジュール( go.mod の あるルート)を丸ごとコピー 初回だけ生成してキャッシュ、以降は使い回 す → 物理的に別の場所を書くので並列でも衝突 しない 同一ファイルには mutex コピーで守れるのはディスクだけ。共有 AST はファイル名ごとの mutexで守る(→ ⑥) 別ファイルなら AST も別の木 → 並列のまま fileLock(m.Position().Filename).Lock() 実装: internal/engine/workdir/workdir.go , internal/engine/workerpool/workerpool.go
⑥ 本番コードの差し替え やりたいのは「トークン1個の変更」。でも実際に触る対象は2つある。 func (m *TokenMutator) Apply() error { //
①ソースファイル単位でロック(worker 横断) fileLock(m.Position().Filename).Lock() defer fileLock(...).Unlock() // 解放は関数を抜けるとき // 書き込み先は worker コピー配下のファイル filename := filepath.Join( m.workDir, m.Position().Filename) // ②ディスクのファイルを退避 m.origFile, _ = os.ReadFile(filename) // ③元トークンを記憶 m.actualToken = m.tokenNode.Tok() // ④共有ASTを変異 m.tokenNode.SetTok(...) // ⑤printerでファイル全書き m.writeMutatedFile(filename) // ⑥共有ASTは即戻す(=ロック解放より前) m.tokenNode.SetTok(m.actualToken) } 実装: internal/engine/tokenmutator.go 触る対象は2つ AST:メモリ上・ファイルごとに1本を worker 横断で共有 ファイル:worker ごとのコピー (ディスク/ m.workDir 配下) 部分置換じゃない AST 全体を go/printer でソースコードに再レ ンダリングして上書き。 文字列 >= を探して置換ではない。
⑥ なぜ「AST は即戻す・ファイルは後で戻す」? 直前で触った2つ(メモリの共有 AST/ディスクのファイル)は、戻すタイミングが逆。 AST(メモリ上・全 worker で共有) 木はファイルごとに1本。同じファイルの mutant
が worker をまたいで共有 変異→書き出し→復元を同じロック区間で完 結 他の mutant と干渉させない=原子性 → だから Apply を抜けた時点で AST は元通 り ファイル(worker コピー・ディスク) go test が読むのはディスク上のこのファ イル だからテスト中は変異したままにする テスト後に Rollback() で復元(=ステップ ⑧) func (m *TokenMutator) Rollback() error { return os.WriteFile( filename, m.origFile, 0o600) // 退避を書戻し } 実装: internal/engine/tokenmutator.go ( Rollback )/呼び出し順は internal/engine/executor.go
⑦ テストを回して判定する 実行するコマンド go test -timeout <t> -failfast <pkg> 対象は変異したパッケージだけ(全体は回さない
→ 速い) タイムアウト = カバレッジ取得の実時間 × 係数 (既定5) カバーされていない行の mutant はテストせずス キップ 実装: internal/engine/executor.go 終了コードで判定 結果 exit 1(テスト失敗) 成功(変異が素通り) タイムアウト exit 2(ビルド不能) 判定 KILLED ✅ LIVED TIMED OUT NOT VIABLE
まとめ 差し替えの仕組み AST(トークン)ベース。型チェックはしな い 対象トークンを1個変えて go/printer でフ ァイル丸ごと再生成 AST は即戻し/ファイルはバックアップから
安全と速度の工夫 カバレッジでテスト対象の行だけ変異 変異パッケージだけを go test -failfast worker ごとに作業コピー+ファイル mutex で 並列 Rollback 👾ご清聴ありがとうございました!👾
想定 Q&A | Q1 Q. AST は worker ごとにコピーされる? A.
いいえ。メモリ上の1本を全 worker で共有 AST はファイルごとにメモリ上に1本だけ。全 worker が共有する Gremlins は1プロセスで動き、worker は goroutine → 同じ AST(同じポインタ)を触る worker ごとにコピーされるのはディスク上のファイルの方
想定 Q&A | Q2 Q. なぜ AST を共有するの? 速いから? A.
速さのためではなく、設計の帰結 変異点を AST ノードのポインタで持つ設計 → パースした木をそのまま使い回す=共有になる パースは go test に比べ一瞬。コピーして避けるほどでもない 共有の代償としてファイル単位ロック+即戻しを払っている
想定 Q&A | Q3 Q. AST があるのに、なぜファイルを丸ごとコピーする? A. go test
はディスクの実体で動くから はディスク上のソースをコンパイル&実行する テスト( _test.go )・ go.mod ・依存パッケージなどプロジェクト全体が必要 AST は「変異する1ファイルの中身」を作るだけ → だからツリーごとコピー go test
想定 Q&A | Q4 Q. どのテストが mutant を殺したかは分かる? A. 分からない。
go test の終了コードしか見ない設計 判定は終了コードだけ(0 = LIVED / 1 = KILLED / 2 = NOT VIABLE)。テストの出力は読み捨て -failfast で最初の失敗で打ち切る → 2件目以降の結果はそもそも生成されない PIT(Java)や Stryker(JS)は killing test をレポートに出せる。gremlins はその観測を捨てて速度を 取る割り切り
想定 Q&A | Q5 Q. -failfast で打ち切ったら、他の mutant の結果が見れなくならない? A.
ならない。効くのは mutant 1個のテスト実行の内側だけ レポートの1行 = mutant 1個 = 独立した go test 1回。打ち切られても次の mutant はまっさらな状態 から丸ごと走る KILLED の行ほど早く打ち切られ、LIVED の行だけがスイートを完走している NOT COVERED はさらに手前で、 go test 自体を走らせていない(カバレッジで判断)