Skip to content

Commit 221c3df

Browse files
craig[bot]stevendanna
andcommitted
Merge #148634
148634: kvnemesis: first step towards a fuzzed KVNemesis r=miraradeva a=stevendanna You can run this test with the go fuzzer with something like: go test ./pkg/kv/kvnemesis/ -test.fuzz=FuzzKVNemesisSingleNode \ -test.fuzzcachedir=_fuzzcache -v -test.run=^$ \ -tags crdb_test -timeout=300m -parallel=4 It can also be run under bazel, but I have not yet sorted out all of the flags needed to get a coverage enabled build and to ensure that the failing test cases get written somewhere that can be referenced on subsequent runs. The idea here is that the fuzzer provides a []byte that then determines the output of all random decisions in KVNemesis. This doesn't account for metamorphic decisions made outside of KVNemesis. KVNemesis is a rather heavyweight test which seemed to be a problem for running it reliably under go-fuzz; however, go-fuzz's poor diagnostics when the test worker crash has made it hard to determine the exact cause so far. Epic: none Release note: None Co-authored-by: Steven Danna <[email protected]>
2 parents 6e1f703 + ee29395 commit 221c3df

File tree

3 files changed

+161
-9
lines changed

3 files changed

+161
-9
lines changed

pkg/kv/kvnemesis/kvnemesis_test.go

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -209,16 +209,24 @@ func randWithSeed(
209209
t interface {
210210
Logf(string, ...interface{})
211211
Helper()
212-
}, seedOrZero int64,
212+
}, cfg kvnemesisTestCfg,
213213
) (*rand.Rand, counter, int64) {
214214
t.Helper()
215+
215216
var rngSource rand.Source
216-
if seedOrZero > 0 {
217-
rngSource = rand.NewSource(seedOrZero)
217+
seedOrZero := cfg.seedOverride
218+
if cfg.randSource != nil {
219+
rngSource = cfg.randSource
220+
t.Logf("using config-supplied random source, seed ignored")
218221
} else {
219-
rngSource, seedOrZero = randutil.NewTestRandSource()
222+
if seedOrZero > 0 {
223+
rngSource = rand.NewSource(seedOrZero)
224+
} else {
225+
rngSource, seedOrZero = randutil.NewTestRandSource()
226+
}
227+
t.Logf("seed: %d", seedOrZero)
220228
}
221-
t.Logf("seed: %d", seedOrZero)
229+
222230
countingSource := newCountingSource(rngSource.(rand.Source64))
223231
return rand.New(countingSource), countingSource, seedOrZero
224232
}
@@ -233,7 +241,7 @@ type tBridge struct {
233241
ll logLogger
234242
}
235243

236-
func newTBridge(t *testing.T) *tBridge {
244+
func newTBridge(t testing.TB) *tBridge {
237245
// NB: we're not using t.TempDir() because we want these to survive
238246
// on failure.
239247
td, err := os.MkdirTemp(datapathutils.DebuggableTempDir(), "kvnemesis")
@@ -263,6 +271,7 @@ type kvnemesisTestCfg struct {
263271
numNodes int
264272
numSteps int
265273
concurrency int
274+
randSource rand.Source
266275
seedOverride int64
267276
// The two knobs below inject illegal lease index errors and, for the
268277
// resulting reproposals, reproposal errors. The injection is stateful and
@@ -418,6 +427,50 @@ func TestKVNemesisMultiNode(t *testing.T) {
418427
})
419428
}
420429

430+
// FuzzKVNemesisSingleNode is an attempt ot make it possible to run KVNemesis
431+
// with a coverage-guided fuzzer. It takes in []bytes as input and then uses
432+
// this to feed all random decisions in the test.
433+
func FuzzKVNemesisSingleNode(f *testing.F) {
434+
defer leaktest.AfterTest(f)()
435+
defer log.Scope(f).Close(f)
436+
437+
const (
438+
// Set to > 0 to pre-generate corpus data.
439+
corpusSize = 0
440+
// I've set these to low values for now to at least get things running
441+
// reliably. With all default settings the test runner fails without
442+
// printing any useful info. I _think_ it might be the result of a
443+
// hard-coded 10s timeout in the go-fuzz test worker.
444+
numStep = 10
445+
concurrency = 1
446+
)
447+
for range corpusSize {
448+
rndSource := randutil.NewRecordingRandSource(rand.NewSource(randutil.NewPseudoSeed()).(rand.Source64))
449+
testKVNemesisImpl(f, kvnemesisTestCfg{
450+
numNodes: 1,
451+
numSteps: numStep,
452+
concurrency: concurrency,
453+
randSource: rndSource,
454+
invalidLeaseAppliedIndexProb: 0.2,
455+
injectReproposalErrorProb: 0.2,
456+
assertRaftApply: true,
457+
})
458+
f.Add(rndSource.Output())
459+
}
460+
461+
f.Fuzz(func(t *testing.T, data []byte) {
462+
testKVNemesisImpl(t, kvnemesisTestCfg{
463+
numNodes: 1,
464+
numSteps: numStep,
465+
concurrency: concurrency,
466+
randSource: randutil.NewFuzzRandSource(t, data),
467+
invalidLeaseAppliedIndexProb: 0.2,
468+
injectReproposalErrorProb: 0.2,
469+
assertRaftApply: true,
470+
})
471+
})
472+
}
473+
421474
func TestKVNemesisMultiNode_LeaderLeases(t *testing.T) {
422475
defer leaktest.AfterTest(t)()
423476
defer log.Scope(t).Close(t)
@@ -434,7 +487,7 @@ func TestKVNemesisMultiNode_LeaderLeases(t *testing.T) {
434487
})
435488
}
436489

437-
func testKVNemesisImpl(t *testing.T, cfg kvnemesisTestCfg) {
490+
func testKVNemesisImpl(t testing.TB, cfg kvnemesisTestCfg) {
438491
skip.UnderRace(t)
439492

440493
if !buildutil.CrdbTestBuild {
@@ -446,7 +499,7 @@ func testKVNemesisImpl(t *testing.T, cfg kvnemesisTestCfg) {
446499

447500
// Can set a seed here for determinism. This works best when the seed was
448501
// obtained with cfg.concurrency=1.
449-
rng, countingSource, seed := randWithSeed(t, cfg.seedOverride)
502+
rng, countingSource, seed := randWithSeed(t, cfg)
450503

451504
// 4 nodes so we have somewhere to move 3x replicated ranges to.
452505
ctx := context.Background()
@@ -513,7 +566,7 @@ func TestRunReproductionSteps(t *testing.T) {
513566
// Paste a repro as printed by kvnemesis here.
514567
}
515568

516-
func dumpRaftLogsOnFailure(t *testing.T, dir string, srvs []serverutils.TestServerInterface) {
569+
func dumpRaftLogsOnFailure(t testing.TB, dir string, srvs []serverutils.TestServerInterface) {
517570
if !t.Failed() {
518571
return
519572
}

pkg/testutils/lint/lint_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,6 +1395,7 @@ func TestLint(t *testing.T) {
13951395
"--",
13961396
"*.go",
13971397
":!testutils/skip/skip.go",
1398+
":!util/randutil/rand.go",
13981399
":!cmd/roachtest/*.go",
13991400
":!acceptance/compose/*.go",
14001401
":!util/syncutil/*.go",

pkg/util/randutil/rand.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"math/rand"
1414
"runtime"
1515
"strings"
16+
"testing"
1617
"time"
1718
_ "unsafe" // required by go:linkname
1819

@@ -285,3 +286,100 @@ func getTestName() string {
285286
}
286287
return ""
287288
}
289+
290+
// FuzzRandSource is a rand.Source whose output is completely determined by the
291+
// input bytes. This can be used by tests that make random decisions using an
292+
// RNG and want that to be driven by the fuzzer.
293+
//
294+
// Once the input runs out, the given test is marked as skipped and 42 is
295+
// returned.
296+
//
297+
// TODO(ssd): My suspicion is that this is better than simply allowing the
298+
// fuzzer to set the seed of our random number generator. With this, the
299+
// fuzzer's next step can change a single decision without affecting all
300+
// previous decisions in the test, giving it some ability to direct its
301+
// exploration.
302+
type FuzzRandSource struct {
303+
t testing.TB
304+
input []byte
305+
}
306+
307+
var _ rand.Source64 = (*FuzzRandSource)(nil)
308+
309+
func NewFuzzRandSource(t testing.TB, input []byte) *FuzzRandSource {
310+
return &FuzzRandSource{
311+
t: t,
312+
input: input,
313+
}
314+
}
315+
316+
func (s *FuzzRandSource) getBytes(n int) []byte {
317+
if len(s.input) < n {
318+
return nil
319+
}
320+
ret := s.input[0:n]
321+
s.input = s.input[n:]
322+
return ret
323+
}
324+
325+
const (
326+
uint64Size = 8
327+
rngMask = (1 << 63) - 1
328+
outOfInputValue = 42
329+
)
330+
331+
func (s *FuzzRandSource) Int63() int64 {
332+
return int64(s.Uint64() & rngMask)
333+
}
334+
335+
func (s *FuzzRandSource) Uint64() uint64 {
336+
data := s.getBytes(uint64Size)
337+
if data == nil {
338+
s.t.Skip("insufficient input bytes")
339+
return outOfInputValue
340+
}
341+
return binary.LittleEndian.Uint64(data)
342+
}
343+
344+
// Seed does nothing for FuzzRandSource.
345+
func (s *FuzzRandSource) Seed(int64) {}
346+
347+
// RecordingRandSource records the output of the inner source. The intended use
348+
// is to produce a "corpus" for a fuzzer that will use FuzzRandSource.
349+
//
350+
// No work has been put into allocating the output efficiently.
351+
type RecordingRandSource struct {
352+
inner rand.Source64
353+
output []byte
354+
}
355+
356+
func NewRecordingRandSource(source rand.Source64) *RecordingRandSource {
357+
return &RecordingRandSource{
358+
inner: source,
359+
output: make([]byte, 0, uint64Size*32),
360+
}
361+
}
362+
363+
func (s *RecordingRandSource) putUint64(n uint64) {
364+
start := len(s.output)
365+
s.output = append(s.output, make([]byte, uint64Size)...)
366+
binary.LittleEndian.PutUint64(s.output[start:start+uint64Size], n)
367+
}
368+
369+
func (s *RecordingRandSource) Uint64() uint64 {
370+
ret := s.inner.Uint64()
371+
s.putUint64(ret)
372+
return ret
373+
}
374+
375+
func (s *RecordingRandSource) Int63() int64 {
376+
return int64(s.Uint64() & rngMask)
377+
}
378+
379+
func (s *RecordingRandSource) Seed(seed int64) {
380+
s.inner.Seed(seed)
381+
}
382+
383+
func (s *RecordingRandSource) Output() []byte {
384+
return s.output
385+
}

0 commit comments

Comments
 (0)