| // 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) |
| } |
| }) |
| } |
| }) |
| } |