| // Copyright 2009 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package strings_test | |
| import ( | |
| "bytes" | |
| "fmt" | |
| . "strings" | |
| "testing" | |
| ) | |
| var htmlEscaper = NewReplacer( | |
| "&", "&", | |
| "<", "<", | |
| ">", ">", | |
| `"`, """, | |
| "'", "'", | |
| ) | |
| var htmlUnescaper = NewReplacer( | |
| "&", "&", | |
| "<", "<", | |
| ">", ">", | |
| """, `"`, | |
| "'", "'", | |
| ) | |
| // The http package's old HTML escaping function. | |
| func oldHTMLEscape(s string) string { | |
| s = Replace(s, "&", "&", -1) | |
| s = Replace(s, "<", "<", -1) | |
| s = Replace(s, ">", ">", -1) | |
| s = Replace(s, `"`, """, -1) | |
| s = Replace(s, "'", "'", -1) | |
| return s | |
| } | |
| var capitalLetters = NewReplacer("a", "A", "b", "B") | |
| // TestReplacer tests the replacer implementations. | |
| func TestReplacer(t *testing.T) { | |
| type testCase struct { | |
| r *Replacer | |
| in, out string | |
| } | |
| var testCases []testCase | |
| // str converts 0xff to "\xff". This isn't just string(b) since that converts to UTF-8. | |
| str := func(b byte) string { | |
| return string([]byte{b}) | |
| } | |
| var s []string | |
| // inc maps "\x00"->"\x01", ..., "a"->"b", "b"->"c", ..., "\xff"->"\x00". | |
| s = nil | |
| for i := 0; i < 256; i++ { | |
| s = append(s, str(byte(i)), str(byte(i+1))) | |
| } | |
| inc := NewReplacer(s...) | |
| // Test cases with 1-byte old strings, 1-byte new strings. | |
| testCases = append(testCases, | |
| testCase{capitalLetters, "brad", "BrAd"}, | |
| testCase{capitalLetters, Repeat("a", (32<<10)+123), Repeat("A", (32<<10)+123)}, | |
| testCase{capitalLetters, "", ""}, | |
| testCase{inc, "brad", "csbe"}, | |
| testCase{inc, "\x00\xff", "\x01\x00"}, | |
| testCase{inc, "", ""}, | |
| testCase{NewReplacer("a", "1", "a", "2"), "brad", "br1d"}, | |
| ) | |
| // repeat maps "a"->"a", "b"->"bb", "c"->"ccc", ... | |
| s = nil | |
| for i := 0; i < 256; i++ { | |
| n := i + 1 - 'a' | |
| if n < 1 { | |
| n = 1 | |
| } | |
| s = append(s, str(byte(i)), Repeat(str(byte(i)), n)) | |
| } | |
| repeat := NewReplacer(s...) | |
| // Test cases with 1-byte old strings, variable length new strings. | |
| testCases = append(testCases, | |
| testCase{htmlEscaper, "No changes", "No changes"}, | |
| testCase{htmlEscaper, "I <3 escaping & stuff", "I <3 escaping & stuff"}, | |
| testCase{htmlEscaper, "&&&", "&&&"}, | |
| testCase{htmlEscaper, "", ""}, | |
| testCase{repeat, "brad", "bbrrrrrrrrrrrrrrrrrradddd"}, | |
| testCase{repeat, "abba", "abbbba"}, | |
| testCase{repeat, "", ""}, | |
| testCase{NewReplacer("a", "11", "a", "22"), "brad", "br11d"}, | |
| ) | |
| // The remaining test cases have variable length old strings. | |
| testCases = append(testCases, | |
| testCase{htmlUnescaper, "&amp;", "&"}, | |
| testCase{htmlUnescaper, "<b>HTML's neat</b>", "<b>HTML's neat</b>"}, | |
| testCase{htmlUnescaper, "", ""}, | |
| testCase{NewReplacer("a", "1", "a", "2", "xxx", "xxx"), "brad", "br1d"}, | |
| testCase{NewReplacer("a", "1", "aa", "2", "aaa", "3"), "aaaa", "1111"}, | |
| testCase{NewReplacer("aaa", "3", "aa", "2", "a", "1"), "aaaa", "31"}, | |
| ) | |
| // gen1 has multiple old strings of variable length. There is no | |
| // overall non-empty common prefix, but some pairwise common prefixes. | |
| gen1 := NewReplacer( | |
| "aaa", "3[aaa]", | |
| "aa", "2[aa]", | |
| "a", "1[a]", | |
| "i", "i", | |
| "longerst", "most long", | |
| "longer", "medium", | |
| "long", "short", | |
| "xx", "xx", | |
| "x", "X", | |
| "X", "Y", | |
| "Y", "Z", | |
| ) | |
| testCases = append(testCases, | |
| testCase{gen1, "fooaaabar", "foo3[aaa]b1[a]r"}, | |
| testCase{gen1, "long, longerst, longer", "short, most long, medium"}, | |
| testCase{gen1, "xxxxx", "xxxxX"}, | |
| testCase{gen1, "XiX", "YiY"}, | |
| testCase{gen1, "", ""}, | |
| ) | |
| // gen2 has multiple old strings with no pairwise common prefix. | |
| gen2 := NewReplacer( | |
| "roses", "red", | |
| "violets", "blue", | |
| "sugar", "sweet", | |
| ) | |
| testCases = append(testCases, | |
| testCase{gen2, "roses are red, violets are blue...", "red are red, blue are blue..."}, | |
| testCase{gen2, "", ""}, | |
| ) | |
| // gen3 has multiple old strings with an overall common prefix. | |
| gen3 := NewReplacer( | |
| "abracadabra", "poof", | |
| "abracadabrakazam", "splat", | |
| "abraham", "lincoln", | |
| "abrasion", "scrape", | |
| "abraham", "isaac", | |
| ) | |
| testCases = append(testCases, | |
| testCase{gen3, "abracadabrakazam abraham", "poofkazam lincoln"}, | |
| testCase{gen3, "abrasion abracad", "scrape abracad"}, | |
| testCase{gen3, "abba abram abrasive", "abba abram abrasive"}, | |
| testCase{gen3, "", ""}, | |
| ) | |
| // foo{1,2,3,4} have multiple old strings with an overall common prefix | |
| // and 1- or 2- byte extensions from the common prefix. | |
| foo1 := NewReplacer( | |
| "foo1", "A", | |
| "foo2", "B", | |
| "foo3", "C", | |
| ) | |
| foo2 := NewReplacer( | |
| "foo1", "A", | |
| "foo2", "B", | |
| "foo31", "C", | |
| "foo32", "D", | |
| ) | |
| foo3 := NewReplacer( | |
| "foo11", "A", | |
| "foo12", "B", | |
| "foo31", "C", | |
| "foo32", "D", | |
| ) | |
| foo4 := NewReplacer( | |
| "foo12", "B", | |
| "foo32", "D", | |
| ) | |
| testCases = append(testCases, | |
| testCase{foo1, "fofoofoo12foo32oo", "fofooA2C2oo"}, | |
| testCase{foo1, "", ""}, | |
| testCase{foo2, "fofoofoo12foo32oo", "fofooA2Doo"}, | |
| testCase{foo2, "", ""}, | |
| testCase{foo3, "fofoofoo12foo32oo", "fofooBDoo"}, | |
| testCase{foo3, "", ""}, | |
| testCase{foo4, "fofoofoo12foo32oo", "fofooBDoo"}, | |
| testCase{foo4, "", ""}, | |
| ) | |
| // genAll maps "\x00\x01\x02...\xfe\xff" to "[all]", amongst other things. | |
| allBytes := make([]byte, 256) | |
| for i := range allBytes { | |
| allBytes[i] = byte(i) | |
| } | |
| allString := string(allBytes) | |
| genAll := NewReplacer( | |
| allString, "[all]", | |
| "\xff", "[ff]", | |
| "\x00", "[00]", | |
| ) | |
| testCases = append(testCases, | |
| testCase{genAll, allString, "[all]"}, | |
| testCase{genAll, "a\xff" + allString + "\x00", "a[ff][all][00]"}, | |
| testCase{genAll, "", ""}, | |
| ) | |
| // Test cases with empty old strings. | |
| blankToX1 := NewReplacer("", "X") | |
| blankToX2 := NewReplacer("", "X", "", "") | |
| blankHighPriority := NewReplacer("", "X", "o", "O") | |
| blankLowPriority := NewReplacer("o", "O", "", "X") | |
| blankNoOp1 := NewReplacer("", "") | |
| blankNoOp2 := NewReplacer("", "", "", "A") | |
| blankFoo := NewReplacer("", "X", "foobar", "R", "foobaz", "Z") | |
| testCases = append(testCases, | |
| testCase{blankToX1, "foo", "XfXoXoX"}, | |
| testCase{blankToX1, "", "X"}, | |
| testCase{blankToX2, "foo", "XfXoXoX"}, | |
| testCase{blankToX2, "", "X"}, | |
| testCase{blankHighPriority, "oo", "XOXOX"}, | |
| testCase{blankHighPriority, "ii", "XiXiX"}, | |
| testCase{blankHighPriority, "oiio", "XOXiXiXOX"}, | |
| testCase{blankHighPriority, "iooi", "XiXOXOXiX"}, | |
| testCase{blankHighPriority, "", "X"}, | |
| testCase{blankLowPriority, "oo", "OOX"}, | |
| testCase{blankLowPriority, "ii", "XiXiX"}, | |
| testCase{blankLowPriority, "oiio", "OXiXiOX"}, | |
| testCase{blankLowPriority, "iooi", "XiOOXiX"}, | |
| testCase{blankLowPriority, "", "X"}, | |
| testCase{blankNoOp1, "foo", "foo"}, | |
| testCase{blankNoOp1, "", ""}, | |
| testCase{blankNoOp2, "foo", "foo"}, | |
| testCase{blankNoOp2, "", ""}, | |
| testCase{blankFoo, "foobarfoobaz", "XRXZX"}, | |
| testCase{blankFoo, "foobar-foobaz", "XRX-XZX"}, | |
| testCase{blankFoo, "", "X"}, | |
| ) | |
| // single string replacer | |
| abcMatcher := NewReplacer("abc", "[match]") | |
| testCases = append(testCases, | |
| testCase{abcMatcher, "", ""}, | |
| testCase{abcMatcher, "ab", "ab"}, | |
| testCase{abcMatcher, "abc", "[match]"}, | |
| testCase{abcMatcher, "abcd", "[match]d"}, | |
| testCase{abcMatcher, "cabcabcdabca", "c[match][match]d[match]a"}, | |
| ) | |
| // Issue 6659 cases (more single string replacer) | |
| noHello := NewReplacer("Hello", "") | |
| testCases = append(testCases, | |
| testCase{noHello, "Hello", ""}, | |
| testCase{noHello, "Hellox", "x"}, | |
| testCase{noHello, "xHello", "x"}, | |
| testCase{noHello, "xHellox", "xx"}, | |
| ) | |
| // No-arg test cases. | |
| nop := NewReplacer() | |
| testCases = append(testCases, | |
| testCase{nop, "abc", "abc"}, | |
| testCase{nop, "", ""}, | |
| ) | |
| // Run the test cases. | |
| for i, tc := range testCases { | |
| if s := tc.r.Replace(tc.in); s != tc.out { | |
| t.Errorf("%d. Replace(%q) = %q, want %q", i, tc.in, s, tc.out) | |
| } | |
| var buf bytes.Buffer | |
| n, err := tc.r.WriteString(&buf, tc.in) | |
| if err != nil { | |
| t.Errorf("%d. WriteString: %v", i, err) | |
| continue | |
| } | |
| got := buf.String() | |
| if got != tc.out { | |
| t.Errorf("%d. WriteString(%q) wrote %q, want %q", i, tc.in, got, tc.out) | |
| continue | |
| } | |
| if n != len(tc.out) { | |
| t.Errorf("%d. WriteString(%q) wrote correct string but reported %d bytes; want %d (%q)", | |
| i, tc.in, n, len(tc.out), tc.out) | |
| } | |
| } | |
| } | |
| var algorithmTestCases = []struct { | |
| r *Replacer | |
| want string | |
| }{ | |
| {capitalLetters, "*strings.byteReplacer"}, | |
| {htmlEscaper, "*strings.byteStringReplacer"}, | |
| {NewReplacer("12", "123"), "*strings.singleStringReplacer"}, | |
| {NewReplacer("1", "12"), "*strings.byteStringReplacer"}, | |
| {NewReplacer("", "X"), "*strings.genericReplacer"}, | |
| {NewReplacer("a", "1", "b", "12", "cde", "123"), "*strings.genericReplacer"}, | |
| } | |
| // TestPickAlgorithm tests that NewReplacer picks the correct algorithm. | |
| func TestPickAlgorithm(t *testing.T) { | |
| for i, tc := range algorithmTestCases { | |
| got := fmt.Sprintf("%T", tc.r.Replacer()) | |
| if got != tc.want { | |
| t.Errorf("%d. algorithm = %s, want %s", i, got, tc.want) | |
| } | |
| } | |
| } | |
| type errWriter struct{} | |
| func (errWriter) Write(p []byte) (n int, err error) { | |
| return 0, fmt.Errorf("unwritable") | |
| } | |
| // TestWriteStringError tests that WriteString returns an error | |
| // received from the underlying io.Writer. | |
| func TestWriteStringError(t *testing.T) { | |
| for i, tc := range algorithmTestCases { | |
| n, err := tc.r.WriteString(errWriter{}, "abc") | |
| if n != 0 || err == nil || err.Error() != "unwritable" { | |
| t.Errorf("%d. WriteStringError = %d, %v, want 0, unwritable", i, n, err) | |
| } | |
| } | |
| } | |
| // TestGenericTrieBuilding verifies the structure of the generated trie. There | |
| // is one node per line, and the key ending with the current line is in the | |
| // trie if it ends with a "+". | |
| func TestGenericTrieBuilding(t *testing.T) { | |
| testCases := []struct{ in, out string }{ | |
| {"abc;abdef;abdefgh;xx;xy;z", `- | |
| a- | |
| .b- | |
| ..c+ | |
| ..d- | |
| ...ef+ | |
| .....gh+ | |
| x- | |
| .x+ | |
| .y+ | |
| z+ | |
| `}, | |
| {"abracadabra;abracadabrakazam;abraham;abrasion", `- | |
| a- | |
| .bra- | |
| ....c- | |
| .....adabra+ | |
| ...........kazam+ | |
| ....h- | |
| .....am+ | |
| ....s- | |
| .....ion+ | |
| `}, | |
| {"aaa;aa;a;i;longerst;longer;long;xx;x;X;Y", `- | |
| X+ | |
| Y+ | |
| a+ | |
| .a+ | |
| ..a+ | |
| i+ | |
| l- | |
| .ong+ | |
| ....er+ | |
| ......st+ | |
| x+ | |
| .x+ | |
| `}, | |
| {"foo;;foo;foo1", `+ | |
| f- | |
| .oo+ | |
| ...1+ | |
| `}, | |
| } | |
| for _, tc := range testCases { | |
| keys := Split(tc.in, ";") | |
| args := make([]string, len(keys)*2) | |
| for i, key := range keys { | |
| args[i*2] = key | |
| } | |
| got := NewReplacer(args...).PrintTrie() | |
| // Remove tabs from tc.out | |
| wantbuf := make([]byte, 0, len(tc.out)) | |
| for i := 0; i < len(tc.out); i++ { | |
| if tc.out[i] != '\t' { | |
| wantbuf = append(wantbuf, tc.out[i]) | |
| } | |
| } | |
| want := string(wantbuf) | |
| if got != want { | |
| t.Errorf("PrintTrie(%q)\ngot\n%swant\n%s", tc.in, got, want) | |
| } | |
| } | |
| } | |
| func BenchmarkGenericNoMatch(b *testing.B) { | |
| str := Repeat("A", 100) + Repeat("B", 100) | |
| generic := NewReplacer("a", "A", "b", "B", "12", "123") // varying lengths forces generic | |
| for i := 0; i < b.N; i++ { | |
| generic.Replace(str) | |
| } | |
| } | |
| func BenchmarkGenericMatch1(b *testing.B) { | |
| str := Repeat("a", 100) + Repeat("b", 100) | |
| generic := NewReplacer("a", "A", "b", "B", "12", "123") | |
| for i := 0; i < b.N; i++ { | |
| generic.Replace(str) | |
| } | |
| } | |
| func BenchmarkGenericMatch2(b *testing.B) { | |
| str := Repeat("It's <b>HTML</b>!", 100) | |
| for i := 0; i < b.N; i++ { | |
| htmlUnescaper.Replace(str) | |
| } | |
| } | |
| func benchmarkSingleString(b *testing.B, pattern, text string) { | |
| r := NewReplacer(pattern, "[match]") | |
| b.SetBytes(int64(len(text))) | |
| b.ResetTimer() | |
| for i := 0; i < b.N; i++ { | |
| r.Replace(text) | |
| } | |
| } | |
| func BenchmarkSingleMaxSkipping(b *testing.B) { | |
| benchmarkSingleString(b, Repeat("b", 25), Repeat("a", 10000)) | |
| } | |
| func BenchmarkSingleLongSuffixFail(b *testing.B) { | |
| benchmarkSingleString(b, "b"+Repeat("a", 500), Repeat("a", 1002)) | |
| } | |
| func BenchmarkSingleMatch(b *testing.B) { | |
| benchmarkSingleString(b, "abcdef", Repeat("abcdefghijklmno", 1000)) | |
| } | |
| func BenchmarkByteByteNoMatch(b *testing.B) { | |
| str := Repeat("A", 100) + Repeat("B", 100) | |
| for i := 0; i < b.N; i++ { | |
| capitalLetters.Replace(str) | |
| } | |
| } | |
| func BenchmarkByteByteMatch(b *testing.B) { | |
| str := Repeat("a", 100) + Repeat("b", 100) | |
| for i := 0; i < b.N; i++ { | |
| capitalLetters.Replace(str) | |
| } | |
| } | |
| func BenchmarkByteStringMatch(b *testing.B) { | |
| str := "<" + Repeat("a", 99) + Repeat("b", 99) + ">" | |
| for i := 0; i < b.N; i++ { | |
| htmlEscaper.Replace(str) | |
| } | |
| } | |
| func BenchmarkHTMLEscapeNew(b *testing.B) { | |
| str := "I <3 to escape HTML & other text too." | |
| for i := 0; i < b.N; i++ { | |
| htmlEscaper.Replace(str) | |
| } | |
| } | |
| func BenchmarkHTMLEscapeOld(b *testing.B) { | |
| str := "I <3 to escape HTML & other text too." | |
| for i := 0; i < b.N; i++ { | |
| oldHTMLEscape(str) | |
| } | |
| } | |
| func BenchmarkByteStringReplacerWriteString(b *testing.B) { | |
| str := Repeat("I <3 to escape HTML & other text too.", 100) | |
| buf := new(bytes.Buffer) | |
| for i := 0; i < b.N; i++ { | |
| htmlEscaper.WriteString(buf, str) | |
| buf.Reset() | |
| } | |
| } | |
| func BenchmarkByteReplacerWriteString(b *testing.B) { | |
| str := Repeat("abcdefghijklmnopqrstuvwxyz", 100) | |
| buf := new(bytes.Buffer) | |
| for i := 0; i < b.N; i++ { | |
| capitalLetters.WriteString(buf, str) | |
| buf.Reset() | |
| } | |
| } | |
| // BenchmarkByteByteReplaces compares byteByteImpl against multiple Replaces. | |
| func BenchmarkByteByteReplaces(b *testing.B) { | |
| str := Repeat("a", 100) + Repeat("b", 100) | |
| for i := 0; i < b.N; i++ { | |
| Replace(Replace(str, "a", "A", -1), "b", "B", -1) | |
| } | |
| } | |
| // BenchmarkByteByteMap compares byteByteImpl against Map. | |
| func BenchmarkByteByteMap(b *testing.B) { | |
| str := Repeat("a", 100) + Repeat("b", 100) | |
| fn := func(r rune) rune { | |
| switch r { | |
| case 'a': | |
| return 'A' | |
| case 'b': | |
| return 'B' | |
| } | |
| return r | |
| } | |
| for i := 0; i < b.N; i++ { | |
| Map(fn, str) | |
| } | |
| } | |
| var mapdata = []struct{ name, data string }{ | |
| {"ASCII", "a b c d e f g h i j k l m n o p q r s t u v w x y z"}, | |
| {"Greek", "α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ ς σ τ υ φ χ ψ ω"}, | |
| } | |
| func BenchmarkMap(b *testing.B) { | |
| mapidentity := func(r rune) rune { | |
| return r | |
| } | |
| b.Run("identity", func(b *testing.B) { | |
| for _, md := range mapdata { | |
| b.Run(md.name, func(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| Map(mapidentity, md.data) | |
| } | |
| }) | |
| } | |
| }) | |
| mapchange := func(r rune) rune { | |
| if 'a' <= r && r <= 'z' { | |
| return r + 'A' - 'a' | |
| } | |
| if 'α' <= r && r <= 'ω' { | |
| return r + 'Α' - 'α' | |
| } | |
| return r | |
| } | |
| b.Run("change", func(b *testing.B) { | |
| for _, md := range mapdata { | |
| b.Run(md.name, func(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| Map(mapchange, md.data) | |
| } | |
| }) | |
| } | |
| }) | |
| } |