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  }