-
Notifications
You must be signed in to change notification settings - Fork 468
/
external_test.go
119 lines (106 loc) · 4.19 KB
/
external_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// Copyright 2024 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package pebble_test
import (
"bytes"
"io"
"math/rand/v2"
"strings"
"testing"
"time"
"github.com/cockroachdb/pebble/metamorphic"
"github.com/cockroachdb/pebble/vfs"
"github.com/cockroachdb/pebble/vfs/errorfs"
"github.com/stretchr/testify/require"
)
// TestIteratorErrors is a randomized test designed to ensure that errors
// encountered by reads are properly propagated through to the user. It uses the
// metamorphic tests configured with only write operations to first generate a
// random database. It then uses the metamorphic tests to run a random set of
// read operations against the generated database, randomly injecting errors at
// the VFS layer. If an error is injected over the course of an operation, it
// expects the error to surface to the operation output. If it doesn't, the test
// fails.
func TestIteratorErrors(t *testing.T) {
seed := time.Now().UnixNano()
t.Logf("Using seed %d", seed)
rng := rand.New(rand.NewPCG(0, uint64(seed)))
// Generate a random database by running the metamorphic test with the
// WriteOpConfig. We'll perform ~10,000 random operations that mutate the
// state of the database.
testOpts := metamorphic.RandomOptions(rng, nil /* custom opt parsers */)
// With even a very small injection probability, it's relatively
// unlikely that pebble.DebugCheckLevels will successfully complete
// without being interrupted by an ErrInjected. Omit these checks.
// TODO(jackson): Alternatively, we could wrap pebble.DebugCheckLevels,
// mark the error value as having originated from CheckLevels, and retry
// at most once. We would need to skip retrying on the second invocation
// of DebugCheckLevels. It's all likely more trouble than it's worth.
testOpts.Opts.DebugCheck = nil
// Disable the physical FS so we don't need to worry about paths down below.
if fs := testOpts.Opts.FS; fs == nil || fs == vfs.Default {
testOpts.Opts.FS = vfs.NewMem()
}
testOpts.Opts.Cache.Ref()
{
test, err := metamorphic.New(
metamorphic.GenerateOps(rng, 10000, metamorphic.WriteOpConfig()),
testOpts, "" /* dir */, io.Discard)
require.NoError(t, err)
require.NoError(t, metamorphic.Execute(test))
}
t.Log("Constructed test database state")
{
testOpts.Opts.DisableTableStats = true
testOpts.Opts.DisableAutomaticCompactions = true
// Create an errorfs injector that injects ErrInjected on 5% of reads.
// Wrap it in both a counter and a toggle so that we a) know whether an
// error was injected over the course of an operation, and b) so that we
// can disable error injection during Open.
predicate := errorfs.And(errorfs.Reads, errorfs.Randomly(0.50, seed))
counter := errorfs.Counter{Injector: errorfs.ErrInjected.If(predicate)}
toggle := errorfs.Toggle{Injector: &counter}
testOpts.Opts.FS = errorfs.Wrap(testOpts.Opts.FS, &toggle)
testOpts.Opts.ReadOnly = true
test, err := metamorphic.New(
metamorphic.GenerateOps(rng, 5000, metamorphic.ReadOpConfig()),
testOpts, "" /* dir */, &testWriter{t: t})
require.NoError(t, err)
defer func() {
if r := recover(); r != nil {
t.Errorf("last injected error: %+v", counter.LastError())
panic(r)
}
}()
// Begin injecting errors.
toggle.On()
prevCount := counter.Load()
more := true
for i := 0; more; i++ {
var operationOutput string
more, operationOutput, err = test.Step()
// test.Step returns an error if the test called Fatalf. Error
// injection should NOT trigger calls to Fatalf.
if err != nil {
t.Fatal(err)
}
newCount := counter.Load()
if diff := newCount - prevCount; diff > 0 {
if !strings.Contains(operationOutput, errorfs.ErrInjected.Error()) {
t.Fatalf("Injected %d errors in op %d but the operation output %q does not contain the injected error: %+v",
diff, i, operationOutput, counter.LastError())
}
}
prevCount = newCount
}
t.Logf("Injected %d errors over the course of the test.", counter.Load())
}
}
type testWriter struct {
t *testing.T
}
func (w *testWriter) Write(b []byte) (int, error) {
w.t.Log(string(bytes.TrimSpace(b)))
return len(b), nil
}