github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/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 // Fatalf implements the pebble.Logger interface. Note that the output is 87 // commented. 88 func (h *history) Fatalf(format string, args ...interface{}) { 89 _ = h.log.Output(2, h.format("// FATAL: ", format, args...)) 90 h.err.Store(errors.Errorf(format, args...)) 91 } 92 93 func (h *history) recorder(thread int, op int) historyRecorder { 94 return historyRecorder{ 95 history: h, 96 op: op, 97 } 98 } 99 100 // historyRecorder pairs a history with an operation, annotating all lines 101 // recorded through it with the operation number. 102 type historyRecorder struct { 103 history *history 104 op int 105 } 106 107 // Recordf records the results of a single operation. 108 func (h historyRecorder) Recordf(format string, args ...interface{}) { 109 h.history.Recordf(h.op, format, args...) 110 } 111 112 // Error returns an error if the test has failed from log output, either a 113 // failure regexp match or a call to Fatalf. 114 func (h historyRecorder) Error() error { 115 return h.history.Error() 116 } 117 118 // CompareHistories takes a slice of file paths containing history files. It 119 // performs a diff comparing the first path to all other paths. CompareHistories 120 // returns the index and diff for the first history that differs. If all the 121 // histories are identical, CompareHistories returns a zero index and an empty 122 // string. 123 func CompareHistories(t TestingT, paths []string) (i int, diff string) { 124 base := readHistory(t, paths[0]) 125 base = reorderHistory(base) 126 127 for i := 1; i < len(paths); i++ { 128 lines := readHistory(t, paths[i]) 129 lines = reorderHistory(lines) 130 diff := difflib.UnifiedDiff{ 131 A: base, 132 B: lines, 133 Context: 5, 134 } 135 text, err := difflib.GetUnifiedDiffString(diff) 136 require.NoError(t, err) 137 if text != "" { 138 return i, text 139 } 140 } 141 return 0, "" 142 } 143 144 // reorderHistory takes lines from a history file and reorders the operation 145 // results to be in the order of the operation index numbers. Runs with more 146 // than 1 thread may produce out-of-order histories. Comment lines must've 147 // already been filtered out. 148 func reorderHistory(lines []string) []string { 149 reordered := make([]string, len(lines)) 150 for _, l := range lines { 151 if cleaned := strings.TrimSpace(l); cleaned == "" { 152 continue 153 } 154 reordered[extractOp(l)] = l 155 } 156 return reordered 157 } 158 159 // extractOp parses out an operation's index from the trailing comment. Every 160 // line of history output is suffixed with a comment containing `#<op>` 161 func extractOp(line string) int { 162 i := strings.LastIndexByte(line, '#') 163 j := strings.IndexFunc(line[i+1:], unicode.IsSpace) 164 if j == -1 { 165 j = len(line[i+1:]) 166 } 167 v, err := strconv.Atoi(line[i+1 : i+1+j]) 168 if err != nil { 169 panic(fmt.Sprintf("unable to parse line %q: %s", line, err)) 170 } 171 return v 172 } 173 174 // Read a history file, stripping out lines that begin with a comment. 175 func readHistory(t TestingT, historyPath string) []string { 176 data, err := os.ReadFile(historyPath) 177 require.NoError(t, err) 178 lines := difflib.SplitLines(string(data)) 179 newLines := make([]string, 0, len(lines)) 180 for _, line := range lines { 181 if strings.HasPrefix(line, "// ") { 182 continue 183 } 184 newLines = append(newLines, line) 185 } 186 return newLines 187 }