LCOV - code coverage report
Current view: top level - pebble/metamorphic - history.go (source / functions) Hit Total Coverage
Test: 2023-09-10 08:16Z 1efa535d - tests + meta.lcov Lines: 68 94 72.3 %
Date: 2023-09-10 08:17:56 Functions: 0 0 -

          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             : }

Generated by: LCOV version 1.14