github.com/zuoyebang/bitalostable@v1.0.1-0.20240229032404-e3b99a834294/internal/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  	"io/ioutil"
    11  	"log"
    12  	"regexp"
    13  	"strings"
    14  	"sync/atomic"
    15  	"testing"
    16  
    17  	"github.com/cockroachdb/errors"
    18  	"github.com/pmezard/go-difflib/difflib"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  // history records the results of running a series of operations.
    23  //
    24  // history also implements the bitalostable.Logger interface, outputting to a stdlib
    25  // logger, prefixing the log messages with "//"-style comments.
    26  type history struct {
    27  	err    atomic.Value
    28  	failRE *regexp.Regexp
    29  	log    *log.Logger
    30  	seq    int
    31  }
    32  
    33  func newHistory(failRE string, writers ...io.Writer) *history {
    34  	h := &history{}
    35  	if len(failRE) > 0 {
    36  		h.failRE = regexp.MustCompile(failRE)
    37  	}
    38  	h.log = log.New(io.MultiWriter(writers...), "", 0)
    39  	return h
    40  }
    41  
    42  // Recordf records the results of a single operation.
    43  func (h *history) Recordf(format string, args ...interface{}) {
    44  	if strings.Contains(format, "\n") {
    45  		// We could remove this restriction but suffixing every line with "#<seq>".
    46  		panic(fmt.Sprintf("format string must not contain \\n: %q", format))
    47  	}
    48  
    49  	// We suffix every line with #<seq> in order to provide a marker to locate
    50  	// the line using the diff output. This is necessary because the diff of two
    51  	// histories is done after stripping comment lines (`// ...`) from the
    52  	// history output, which ruins the line number information in the diff
    53  	// output.
    54  	h.seq++
    55  	m := fmt.Sprintf(format, args...) + fmt.Sprintf(" #%d", h.seq)
    56  	h.log.Print(m)
    57  
    58  	if h.failRE != nil && h.failRE.MatchString(m) {
    59  		err := errors.Errorf("failure regexp %q matched output: %s", h.failRE, m)
    60  		h.err.Store(err)
    61  	}
    62  }
    63  
    64  // Error returns an error if the test has failed from log output, either a
    65  // failure regexp match or a call to Fatalf.
    66  func (h *history) Error() error {
    67  	if v := h.err.Load(); v != nil {
    68  		return v.(error)
    69  	}
    70  	return nil
    71  }
    72  
    73  func (h *history) format(prefix, format string, args ...interface{}) string {
    74  	var buf strings.Builder
    75  	orig := fmt.Sprintf(format, args...)
    76  	for _, line := range strings.Split(strings.TrimSpace(orig), "\n") {
    77  		buf.WriteString(prefix)
    78  		buf.WriteString(line)
    79  		buf.WriteString("\n")
    80  	}
    81  	return buf.String()
    82  }
    83  
    84  // Infof implements the bitalostable.Logger interface. Note that the output is
    85  // commented.
    86  func (h *history) Infof(format string, args ...interface{}) {
    87  	_ = h.log.Output(2, h.format("// INFO: ", format, args...))
    88  }
    89  
    90  // Fatalf implements the bitalostable.Logger interface. Note that the output is
    91  // commented.
    92  func (h *history) Fatalf(format string, args ...interface{}) {
    93  	_ = h.log.Output(2, h.format("// FATAL: ", format, args...))
    94  	h.err.Store(errors.Errorf(format, args...))
    95  }
    96  
    97  // CompareHistories takes a slice of file paths containing history files. It
    98  // performs a diff comparing the first path to all other paths. CompareHistories
    99  // returns the index and diff for the first history that differs. If all the
   100  // histories are identical, CompareHistories returns a zero index and an empty
   101  // string.
   102  func CompareHistories(t *testing.T, paths []string) (i int, diff string) {
   103  	base := readHistory(t, paths[0])
   104  	for i := 1; i < len(paths); i++ {
   105  		lines := readHistory(t, paths[i])
   106  		diff := difflib.UnifiedDiff{
   107  			A:       base,
   108  			B:       lines,
   109  			Context: 5,
   110  		}
   111  		text, err := difflib.GetUnifiedDiffString(diff)
   112  		require.NoError(t, err)
   113  		if text != "" {
   114  			return i, text
   115  		}
   116  	}
   117  	return 0, ""
   118  }
   119  
   120  // Read a history file, stripping out lines that begin with a comment.
   121  func readHistory(t *testing.T, historyPath string) []string {
   122  	data, err := ioutil.ReadFile(historyPath)
   123  	require.NoError(t, err)
   124  	lines := difflib.SplitLines(string(data))
   125  	newLines := make([]string, 0, len(lines))
   126  	for _, line := range lines {
   127  		if strings.HasPrefix(line, "// ") {
   128  			continue
   129  		}
   130  		newLines = append(newLines, line)
   131  	}
   132  	return newLines
   133  }