正規表現のテストカバレッジを測りたかった話
by
yubessy
Link
Embed
Share
Beginning
This slide
Copy link URL
Copy link URL
Copy iframe embed code
Copy iframe embed code
Copy javascript embed code
Copy javascript embed code
Share
Tweet
Share
Tweet
Slide 1
Slide 1 text
正規表現のテストカバレッジを 測りたかった話 @yubessy 0x64 物語 Reboot #03 " テスト"
Slide 2
Slide 2 text
正規表現書いてますか?
Slide 3
Slide 3 text
テスト書いてますか?
Slide 4
Slide 4 text
正規表現のテスト ↓ これをテストしたい /[a-z]+\.(co|ne)\.jp/ たぶんこんなかんじ assert("hoge.co.jp" =~ /[a-z]+\.(co|ne)\.jp/) assert("hoge.ne.jp" =~ /[a-z]+\.(co|ne)\.jp/)
Slide 5
Slide 5 text
やりたいこと 正規表現のテストカバレッジを測りたい /[a-z]+\.(co|ne)\.jp/ ↓ カバレッジが十分でない例 assert("hoge.co.jp" =~ /[a-z]+\.(co|ne)\.jp/) # "hoge.ne.jp" は? こういうのを数値化したい
Slide 6
Slide 6 text
Q. それくらい見ればわかるやん?
Slide 7
Slide 7 text
A. (?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_ `{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\ x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0 -9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0 -9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3 }(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0 -9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\ [\x01-\x09\x0b\x0c\x0e-\x7f])+)\])
Slide 8
Slide 8 text
A. RFC5322 準拠の email adress (?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_ `{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\ x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0 -9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0 -9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3 }(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0 -9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\ [\x01-\x09\x0b\x0c\x0e-\x7f])+)\]) https://emailregex.com/
Slide 9
Slide 9 text
どうやってカバレッジを測るか? 正規表現 = 状態機械 カバレッジ(C0) = 全てのパスを1 度以上通ったか ※C1 以上はとりあえず今回は無視
Slide 10
Slide 10 text
ちなみにさっきのやつ
Slide 11
Slide 11 text
ここで問題発生 大体の言語では正規表現はライブラリとして提供 -> 内部実装はラップされていて見えない -> 各パスを通った・ 通らないを知るすべがない -> 処理系に手を入れる
Slide 12
Slide 12 text
手頃な処理系を探す旅 PCRE やばい java.util.regex つらい Oniguruma / Onigumo でかい Rust おしい( 僕の力では読めない) Golang <- !!!
Slide 13
Slide 13 text
Golang の正規表現エンジン NFA( 非決定性有限オー トマン) ベー ス アルゴリズムが理解しやすい 計算量が文字数に対して線形 平均的には遅い Pure Go で実装 自分でも読める コー ド量が手頃( エンジン部分は数100 行) https://github.com/golang/go/tree/master/src/regexp
Slide 14
Slide 14 text
Golang の正規表現エンジン re := regexp.MustCompile("/[a-z]+\.(co|ne)\.jp/") re.MatchString("hoge.co.jp") 内部では 1. 正規表現の構文木を作成 ( syntax/parse.go ) 2. 構文木からプログラムを作成 ( syntax/compile.go ) 3. 文字列を入力としてプログラムを実行 ( exec.go )
Slide 15
Slide 15 text
正規表現プログラムの内部表現 プログラム = 文字を1コずつ処理する命令の列 syntax/prog.go type Prog struct { Inst []Inst ... } type Inst struct { Op InstOp // 命令の種類 Out uint32 // 成功時のジャンプ先の命令番号 Arg uint32 // 失敗時のジャンプ先の命令番号( など) Rune []rune // マッチする文字( 文字マッチ命令のみ) }
Slide 16
Slide 16 text
プログラムの内部表現 0: InstFail 0 0 [] // 失敗( 最初はスキップ) 1: InstRune 2 0 ['a', 'z'] // 'a' - 'z' 2: InstAlt 1 3 [] // 命令1, 3 のいずれか 3: InstRune1 4 0 ['.'] // '.' 4: InstCapture 9 2 [] // グルー ピング開始 5: InstRune1 6 0 ['c'] // 'c' 6: InstRune1 10 0 ['o'] // 'o' 7: InstRune1 8 0 ['n'] // 'n' 8: InstRune1 10 0 ['e'] // 'e' 9: InstAlt 5 7 [] // 命令5, 7 のいずれか 10: InstCapture 11 3 [] // グルー ピング終了 11: InstRune1 12 0 ['.'] // '.' 12: InstRune1 13 0 ['j'] // 'j' 13: InstRune1 14 0 ['p'] // 'p' 14: InstMatch 0 0 [] // 成功
Slide 17
Slide 17 text
カバレッジの計算 やりたいこと = 正規表現のカバレッジを測る -> 各命令にテスト済みフラグを用意 type Inst struct { Op InstOp Out uint32 Arg uint32 Rune []rune Flag bool // テスト済みフラグ <- new! }
Slide 18
Slide 18 text
カバレッジの計算 マッチの実行時に通った命令をマー キングしておく // 文字マッチ命令を1 つ実行する関数 (exec.go) func (m *machine) step(...) { ... switch i.Op { ... case syntax.InstRune: add = i.MatchRune(c) case syntax.InstRune1: add = c == i.Rune[0] ... if add { i.Flag = true // マッチが成功したらフラグを立てる } ... }
Slide 19
Slide 19 text
カバレッジの計算 テストケー スを食わせてフラグの立った命令を数える func (re *Regexp) Coverage() (int, int) { s, a := 0, 0 for _, x := range re.prog.Inst { if x.Op == syntax.InstRune || ... { a++ if x.Flag { s++ } } } return s, a }
Slide 20
Slide 20 text
カバレッジの計算 できた!!! func main() { re := regexp.MustCompile(`[a-z]+\.(co|ne)\.jp`) re.MatchString("hoge.co.jp") c, a := re.Coverage() fmt.Printf("%d / %d", c, a) } 7 / 9 ※ 文字マッチ命令の 成功 / 全部 を数えた場合
Slide 21
Slide 21 text
まとめ 正規表現のカバレッジは( 頑張れば) 測れる Golang の正規表現実装は読みやすい テスト回のはずが正規表現回になった