github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/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 "log" 11 "os" 12 "regexp" 13 "strconv" 14 "strings" 15 "sync/atomic" 16 "unicode" 17 18 "github.com/cockroachdb/errors" 19 "github.com/pmezard/go-difflib/difflib" 20 "github.com/stretchr/testify/require" 21 ) 22 23 // history records the results of running a series of operations. 24 // 25 // history also implements the pebble.Logger interface, outputting to a stdlib 26 // logger, prefixing the log messages with "//"-style comments. 27 type history struct { 28 err atomic.Value 29 failRE *regexp.Regexp 30 log *log.Logger 31 } 32 33 func newHistory(failRE *regexp.Regexp, writers ...io.Writer) *history { 34 h := &history{failRE: failRE} 35 h.log = log.New(io.MultiWriter(writers...), "", 0) 36 return h 37 } 38 39 // Recordf records the results of a single operation. 40 func (h *history) Recordf(op int, format string, args ...interface{}) { 41 if strings.Contains(format, "\n") { 42 // We could remove this restriction but suffixing every line with "#<seq>". 43 panic(fmt.Sprintf("format string must not contain \\n: %q", format)) 44 } 45 46 // We suffix every line with #<op> in order to provide a marker to locate 47 // the line using the diff output. This is necessary because the diff of two 48 // histories is done after stripping comment lines (`// ...`) from the 49 // history output, which ruins the line number information in the diff 50 // output. 51 m := fmt.Sprintf(format, args...) + fmt.Sprintf(" #%d", op) 52 h.log.Print(m) 53 54 if h.failRE != nil && h.failRE.MatchString(m) { 55 err := errors.Errorf("failure regexp %q matched output: %s", h.failRE, m) 56 h.err.Store(err) 57 } 58 } 59 60 // Error returns an error if the test has failed from log output, either a 61 // failure regexp match or a call to Fatalf. 62 func (h *history) Error() error { 63 if v := h.err.Load(); v != nil { 64 return v.(error) 65 } 66 return nil 67 } 68 69 func (h *history) format(prefix, format string, args ...interface{}) string { 70 var buf strings.Builder 71 orig := fmt.Sprintf(format, args...) 72 for _, line := range strings.Split(strings.TrimSpace(orig), "\n") { 73 buf.WriteString(prefix) 74 buf.WriteString(line) 75 buf.WriteString("\n") 76 } 77 return buf.String() 78 } 79 80 // Infof implements the pebble.Logger interface. Note that the output is 81 // commented. 82 func (h *history) Infof(format string, args ...interface{}) { 83 _ = h.log.Output(2, h.format("// INFO: ", format, args...)) 84 } 85 86 // Errorf implements the pebble.Logger interface. Note that the output is 87 // commented. 88 func (h *history) Errorf(format string, args ...interface{}) { 89 _ = h.log.Output(2, h.format("// ERROR: ", format, args...)) 90 } 91 92 // Fatalf implements the pebble.Logger interface. Note that the output is 93 // commented. 94 func (h *history) Fatalf(format string, args ...interface{}) { 95 _ = h.log.Output(2, h.format("// FATAL: ", format, args...)) 96 h.err.Store(errors.Errorf(format, args...)) 97 } 98 99 func (h *history) recorder(thread int, op int) historyRecorder { 100 return historyRecorder{ 101 history: h, 102 op: op, 103 } 104 } 105 106 // historyRecorder pairs a history with an operation, annotating all lines 107 // recorded through it with the operation number. 108 type historyRecorder struct { 109 history *history 110 op int 111 } 112 113 // Recordf records the results of a single operation. 114 func (h historyRecorder) Recordf(format string, args ...interface{}) { 115 h.history.Recordf(h.op, format, args...) 116 } 117 118 // Error returns an error if the test has failed from log output, either a 119 // failure regexp match or a call to Fatalf. 120 func (h historyRecorder) Error() error { 121 return h.history.Error() 122 } 123 124 // CompareHistories takes a slice of file paths containing history files. It 125 // performs a diff comparing the first path to all other paths. CompareHistories 126 // returns the index and diff for the first history that differs. If all the 127 // histories are identical, CompareHistories returns a zero index and an empty 128 // string. 129 func CompareHistories(t TestingT, paths []string) (i int, diff string) { 130 base := readHistory(t, paths[0]) 131 base = reorderHistory(base) 132 133 for i := 1; i < len(paths); i++ { 134 lines := readHistory(t, paths[i]) 135 lines = reorderHistory(lines) 136 diff := difflib.UnifiedDiff{ 137 A: base, 138 B: lines, 139 Context: 5, 140 } 141 text, err := difflib.GetUnifiedDiffString(diff) 142 require.NoError(t, err) 143 if text != "" { 144 return i, text 145 } 146 } 147 return 0, "" 148 } 149 150 // reorderHistory takes lines from a history file and reorders the operation 151 // results to be in the order of the operation index numbers. Runs with more 152 // than 1 thread may produce out-of-order histories. Comment lines must've 153 // already been filtered out. 154 func reorderHistory(lines []string) []string { 155 reordered := make([]string, len(lines)) 156 for _, l := range lines { 157 if cleaned := strings.TrimSpace(l); cleaned == "" { 158 continue 159 } 160 reordered[extractOp(l)] = l 161 } 162 return reordered 163 } 164 165 // extractOp parses out an operation's index from the trailing comment. Every 166 // line of history output is suffixed with a comment containing `#<op>` 167 func extractOp(line string) int { 168 i := strings.LastIndexByte(line, '#') 169 j := strings.IndexFunc(line[i+1:], unicode.IsSpace) 170 if j == -1 { 171 j = len(line[i+1:]) 172 } 173 v, err := strconv.Atoi(line[i+1 : i+1+j]) 174 if err != nil { 175 panic(fmt.Sprintf("unable to parse line %q: %s", line, err)) 176 } 177 return v 178 } 179 180 // Read a history file, stripping out lines that begin with a comment. 181 func readHistory(t TestingT, historyPath string) []string { 182 data, err := os.ReadFile(historyPath) 183 require.NoError(t, err) 184 lines := difflib.SplitLines(string(data)) 185 newLines := make([]string, 0, len(lines)) 186 for _, line := range lines { 187 if strings.HasPrefix(line, "// ") { 188 continue 189 } 190 newLines = append(newLines, line) 191 } 192 return newLines 193 }