Line data Source code
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 1 : func newHistory(failRE *regexp.Regexp, writers ...io.Writer) *history {
34 1 : h := &history{failRE: failRE}
35 1 : h.log = log.New(io.MultiWriter(writers...), "", 0)
36 1 : return h
37 1 : }
38 :
39 : // Recordf records the results of a single operation.
40 1 : func (h *history) Recordf(op int, format string, args ...interface{}) {
41 1 : if strings.Contains(format, "\n") {
42 0 : // We could remove this restriction but suffixing every line with "#<seq>".
43 0 : 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 1 : m := fmt.Sprintf(format, args...) + fmt.Sprintf(" #%d", op)
52 1 : h.log.Print(m)
53 1 :
54 1 : if h.failRE != nil && h.failRE.MatchString(m) {
55 0 : err := errors.Errorf("failure regexp %q matched output: %s", h.failRE, m)
56 0 : h.err.Store(err)
57 0 : }
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 1 : func (h *history) Error() error {
63 1 : if v := h.err.Load(); v != nil {
64 0 : return v.(error)
65 0 : }
66 1 : return nil
67 : }
68 :
69 1 : func (h *history) format(prefix, format string, args ...interface{}) string {
70 1 : var buf strings.Builder
71 1 : orig := fmt.Sprintf(format, args...)
72 1 : for _, line := range strings.Split(strings.TrimSpace(orig), "\n") {
73 1 : buf.WriteString(prefix)
74 1 : buf.WriteString(line)
75 1 : buf.WriteString("\n")
76 1 : }
77 1 : return buf.String()
78 : }
79 :
80 : // Infof implements the pebble.Logger interface. Note that the output is
81 : // commented.
82 1 : func (h *history) Infof(format string, args ...interface{}) {
83 1 : _ = h.log.Output(2, h.format("// INFO: ", format, args...))
84 1 : }
85 :
86 : // Errorf implements the pebble.Logger interface. Note that the output is
87 : // commented.
88 0 : func (h *history) Errorf(format string, args ...interface{}) {
89 0 : _ = h.log.Output(2, h.format("// ERROR: ", format, args...))
90 0 : }
91 :
92 : // Fatalf implements the pebble.Logger interface. Note that the output is
93 : // commented.
94 0 : func (h *history) Fatalf(format string, args ...interface{}) {
95 0 : _ = h.log.Output(2, h.format("// FATAL: ", format, args...))
96 0 : h.err.Store(errors.Errorf(format, args...))
97 0 : }
98 :
99 1 : func (h *history) recorder(thread int, op int) historyRecorder {
100 1 : return historyRecorder{
101 1 : history: h,
102 1 : op: op,
103 1 : }
104 1 : }
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 1 : func (h historyRecorder) Recordf(format string, args ...interface{}) {
115 1 : h.history.Recordf(h.op, format, args...)
116 1 : }
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 0 : func (h historyRecorder) Error() error {
121 0 : return h.history.Error()
122 0 : }
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 0 : func CompareHistories(t TestingT, paths []string) (i int, diff string) {
130 0 : base := readHistory(t, paths[0])
131 0 : base = reorderHistory(base)
132 0 :
133 0 : for i := 1; i < len(paths); i++ {
134 0 : lines := readHistory(t, paths[i])
135 0 : lines = reorderHistory(lines)
136 0 : diff := difflib.UnifiedDiff{
137 0 : A: base,
138 0 : B: lines,
139 0 : Context: 5,
140 0 : }
141 0 : text, err := difflib.GetUnifiedDiffString(diff)
142 0 : require.NoError(t, err)
143 0 : if text != "" {
144 0 : return i, text
145 0 : }
146 : }
147 0 : 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 0 : func reorderHistory(lines []string) []string {
155 0 : reordered := make([]string, len(lines))
156 0 : for _, l := range lines {
157 0 : if cleaned := strings.TrimSpace(l); cleaned == "" {
158 0 : continue
159 : }
160 0 : reordered[extractOp(l)] = l
161 : }
162 0 : 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 0 : func extractOp(line string) int {
168 0 : i := strings.LastIndexByte(line, '#')
169 0 : j := strings.IndexFunc(line[i+1:], unicode.IsSpace)
170 0 : if j == -1 {
171 0 : j = len(line[i+1:])
172 0 : }
173 0 : v, err := strconv.Atoi(line[i+1 : i+1+j])
174 0 : if err != nil {
175 0 : panic(fmt.Sprintf("unable to parse line %q: %s", line, err))
176 : }
177 0 : return v
178 : }
179 :
180 : // Read a history file, stripping out lines that begin with a comment.
181 0 : func readHistory(t TestingT, historyPath string) []string {
182 0 : data, err := os.ReadFile(historyPath)
183 0 : require.NoError(t, err)
184 0 : lines := difflib.SplitLines(string(data))
185 0 : newLines := make([]string, 0, len(lines))
186 0 : for _, line := range lines {
187 0 : if strings.HasPrefix(line, "// ") {
188 0 : continue
189 : }
190 0 : newLines = append(newLines, line)
191 : }
192 0 : return newLines
193 : }
|