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