github.com/zuoyebang/bitalostable@v1.0.1-0.20240229032404-e3b99a834294/internal/metamorphic/history.go (about) 1 // Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use 2 // of this source code is governed by a BSD-style license that can be found in 3 // the LICENSE file. 4 5 package metamorphic 6 7 import ( 8 "fmt" 9 "io" 10 "io/ioutil" 11 "log" 12 "regexp" 13 "strings" 14 "sync/atomic" 15 "testing" 16 17 "github.com/cockroachdb/errors" 18 "github.com/pmezard/go-difflib/difflib" 19 "github.com/stretchr/testify/require" 20 ) 21 22 // history records the results of running a series of operations. 23 // 24 // history also implements the bitalostable.Logger interface, outputting to a stdlib 25 // logger, prefixing the log messages with "//"-style comments. 26 type history struct { 27 err atomic.Value 28 failRE *regexp.Regexp 29 log *log.Logger 30 seq int 31 } 32 33 func newHistory(failRE string, writers ...io.Writer) *history { 34 h := &history{} 35 if len(failRE) > 0 { 36 h.failRE = regexp.MustCompile(failRE) 37 } 38 h.log = log.New(io.MultiWriter(writers...), "", 0) 39 return h 40 } 41 42 // Recordf records the results of a single operation. 43 func (h *history) Recordf(format string, args ...interface{}) { 44 if strings.Contains(format, "\n") { 45 // We could remove this restriction but suffixing every line with "#<seq>". 46 panic(fmt.Sprintf("format string must not contain \\n: %q", format)) 47 } 48 49 // We suffix every line with #<seq> in order to provide a marker to locate 50 // the line using the diff output. This is necessary because the diff of two 51 // histories is done after stripping comment lines (`// ...`) from the 52 // history output, which ruins the line number information in the diff 53 // output. 54 h.seq++ 55 m := fmt.Sprintf(format, args...) + fmt.Sprintf(" #%d", h.seq) 56 h.log.Print(m) 57 58 if h.failRE != nil && h.failRE.MatchString(m) { 59 err := errors.Errorf("failure regexp %q matched output: %s", h.failRE, m) 60 h.err.Store(err) 61 } 62 } 63 64 // Error returns an error if the test has failed from log output, either a 65 // failure regexp match or a call to Fatalf. 66 func (h *history) Error() error { 67 if v := h.err.Load(); v != nil { 68 return v.(error) 69 } 70 return nil 71 } 72 73 func (h *history) format(prefix, format string, args ...interface{}) string { 74 var buf strings.Builder 75 orig := fmt.Sprintf(format, args...) 76 for _, line := range strings.Split(strings.TrimSpace(orig), "\n") { 77 buf.WriteString(prefix) 78 buf.WriteString(line) 79 buf.WriteString("\n") 80 } 81 return buf.String() 82 } 83 84 // Infof implements the bitalostable.Logger interface. Note that the output is 85 // commented. 86 func (h *history) Infof(format string, args ...interface{}) { 87 _ = h.log.Output(2, h.format("// INFO: ", format, args...)) 88 } 89 90 // Fatalf implements the bitalostable.Logger interface. Note that the output is 91 // commented. 92 func (h *history) Fatalf(format string, args ...interface{}) { 93 _ = h.log.Output(2, h.format("// FATAL: ", format, args...)) 94 h.err.Store(errors.Errorf(format, args...)) 95 } 96 97 // CompareHistories takes a slice of file paths containing history files. It 98 // performs a diff comparing the first path to all other paths. CompareHistories 99 // returns the index and diff for the first history that differs. If all the 100 // histories are identical, CompareHistories returns a zero index and an empty 101 // string. 102 func CompareHistories(t *testing.T, paths []string) (i int, diff string) { 103 base := readHistory(t, paths[0]) 104 for i := 1; i < len(paths); i++ { 105 lines := readHistory(t, paths[i]) 106 diff := difflib.UnifiedDiff{ 107 A: base, 108 B: lines, 109 Context: 5, 110 } 111 text, err := difflib.GetUnifiedDiffString(diff) 112 require.NoError(t, err) 113 if text != "" { 114 return i, text 115 } 116 } 117 return 0, "" 118 } 119 120 // Read a history file, stripping out lines that begin with a comment. 121 func readHistory(t *testing.T, historyPath string) []string { 122 data, err := ioutil.ReadFile(historyPath) 123 require.NoError(t, err) 124 lines := difflib.SplitLines(string(data)) 125 newLines := make([]string, 0, len(lines)) 126 for _, line := range lines { 127 if strings.HasPrefix(line, "// ") { 128 continue 129 } 130 newLines = append(newLines, line) 131 } 132 return newLines 133 }