github.com/cockroachdb/pebble@v1.1.2/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  }