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 の正規表現実装は読みやすい テスト回のはずが正規表現回になった