Skip to content

Commit 13060c5

Browse files
committed
Introduce fuzz testing to separate unit tests from repetition tests
Prior to changes introduced here the majority of time during testing was spent on repeating the same test while changing the seed for generators or latency models. Instead of repeating the same test over and over use the fuzz testing mechanism provided by Go SDK. This change allows the unit tests to run much faster while offering a configurable "fuzztime" that dictates for how long the test cases should be fuzzed. The changes here introduce the following fuzz tests: * `FuzzAbsentAdversary` * `FuzzImmediateDecideAdversary` * `FuzzHonest_AsyncRequireStrongQuorumToProgress` * `FuzzHonest_SyncMajorityCommonPrefix` * `FuzzHonest_AsyncMajorityCommonPrefix` * `FuzzHonestMultiInstance_AsyncDisagreement` * `FuzzHonestMultiInstance_SyncAgreement` * `FuzzHonestMultiInstance_AsyncAgreement` * `FuzzStoragePower_SyncIncreaseMidSimulation` * `FuzzStoragePower_AsyncIncreaseMidSimulation` * `FuzzStoragePower_SyncDecreaseRevertsToBase` * `FuzzStoragePower_AsyncDecreaseRevertsToBase` * `FuzzRepeatAdversary` The CI workflow is updated to run each of the fuzz tests for 30 seconds in a dedicated job.
1 parent e228db4 commit 13060c5

15 files changed

+712
-733
lines changed

.github/workflows/ci.yml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,6 @@ jobs:
3838
with:
3939
go-version: '1.21'
4040
- name: Test
41-
env:
42-
# Override test repetition parallelism, since the default runner seem to have a single core.
43-
# This shaves ~30 seconds off the CI test duration.
44-
F3_TEST_REPETITION_PARALLELISM: 2
4541
run: make test/cover
4642
- name: Upload coverage to Codecov
4743
# Commit SHA corresponds to version v4.4.0
@@ -57,6 +53,21 @@ jobs:
5753
token: ${{ secrets.CODECOV_TOKEN }}
5854
fail_ci_if_error: false
5955

56+
fuzz:
57+
name: Fuzz
58+
runs-on: ubuntu-latest
59+
needs:
60+
- test # Do not bother running the fuzz tests if tests fail
61+
steps:
62+
- uses: actions/checkout@v3
63+
- uses: actions/setup-go@v4
64+
with:
65+
go-version: '1.21'
66+
- name: Fuzz
67+
env:
68+
FUZZTIME: 30s
69+
run: make fuzz
70+
6071
generate:
6172
name: Generate
6273
runs-on: ubuntu-latest

Makefile

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
all: generate test lint
1+
SHELL := /usr/bin/env bash
2+
3+
GOGC ?= 1000 # Reduce GC frequency during testing, default to 1000 if unset.
4+
5+
all: generate test fuzz lint
26

3-
test: GOGC ?= 1000 # Reduce the GC frequency, default to 1000 if unset.
47
test:
58
GOGC=$(GOGC) go test $(GOTEST_ARGS) ./...
69
.PHONY: test
@@ -9,8 +12,18 @@ test/cover: test
912
test/cover: GOTEST_ARGS=-coverprofile=coverage.txt -covermode=atomic -coverpkg=./...
1013
.PHONY: test/cover
1114

15+
fuzz: FUZZTIME ?= 10s # The duration to run fuzz testing, default to 10s if unset.
16+
fuzz: # List all fuzz tests across the repo, and run them one at a time with the configured fuzztime.
17+
@go list ./... | while read -r package; do \
18+
go test -list '^Fuzz' "$$package" | grep '^Fuzz' | while read -r func; do \
19+
echo "Running $$package $$func for $(FUZZTIME)..."; \
20+
GOGC=$(GOGC) go test "$$package" -run '^$$' -fuzz="$$func" -fuzztime=$(FUZZTIME) || exit 1; \
21+
done; \
22+
done;
23+
.PHONY: fuzz
1224

1325
lint:
26+
go mod tidy
1427
golangci-lint run ./...
1528
.PHONY: lint
1629

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ require (
1414
github.com/stretchr/testify v1.9.0
1515
github.com/whyrusleeping/cbor-gen v0.1.0
1616
go.uber.org/multierr v1.11.0
17-
golang.org/x/sync v0.3.0
1817
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
1918
)
2019

sim/cmd/f3sim/f3sim.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,8 @@ func main() {
3030
seed := *latencySeed + int64(i)
3131
fmt.Printf("Iteration %d: seed=%d, mean=%f\n", i, seed, *latencyMean)
3232

33-
latencyModel, err := latency.NewLogNormal(*latencySeed, time.Duration(*latencyMean*float64(time.Second)))
34-
if err != nil {
35-
log.Panicf("failed to instantiate log normal latency model: %c\n", err)
36-
}
37-
3833
options := []sim.Option{
39-
sim.WithLatencyModel(latencyModel),
34+
sim.WithLatencyModel(latency.NewLogNormal(*latencySeed, time.Duration(*latencyMean*float64(time.Second)))),
4035
sim.WithECEpochDuration(30 * time.Second),
4136
sim.WithECStabilisationDelay(0),
4237
sim.AddHonestParticipants(

sim/latency/log_normal.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package latency
22

33
import (
4-
"errors"
54
"math"
65
"math/rand"
76
"sync"
@@ -25,16 +24,14 @@ type LogNormal struct {
2524
}
2625

2726
// NewLogNormal instantiates a new latency model of log normal latency
28-
// distribution with the given mean.
29-
func NewLogNormal(seed int64, mean time.Duration) (*LogNormal, error) {
30-
if mean < 0 {
31-
return nil, errors.New("mean duration cannot be negative")
32-
}
27+
// distribution with the given mean. This model will always return zero if mean
28+
// latency duration is less than or equal to zero.
29+
func NewLogNormal(seed int64, mean time.Duration) *LogNormal {
3330
return &LogNormal{
3431
rng: rand.New(rand.NewSource(seed)),
3532
mean: mean,
3633
latencyFromTo: make(map[gpbft.ActorID]map[gpbft.ActorID]time.Duration),
37-
}, nil
34+
}
3835
}
3936

4037
// Sample returns latency samples that correspond to the log normal distribution
@@ -43,9 +40,10 @@ func NewLogNormal(seed int64, mean time.Duration) (*LogNormal, error) {
4340
// distribution. Latency from one participant to another may be asymmetric and
4441
// once generated remains constant for the lifetime of a simulation.
4542
//
46-
// Note, here from and to are the same the latency sample will always be zero.
43+
// Note, where from and to are the same or mean configured latency is not larger
44+
// than zero the latency sample will always be zero.
4745
func (l *LogNormal) Sample(_ time.Time, from gpbft.ActorID, to gpbft.ActorID) time.Duration {
48-
if from == to {
46+
if from == to || l.mean <= 0 {
4947
return 0
5048
}
5149

test/absent_test.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,25 @@ import (
88
"github.com/stretchr/testify/require"
99
)
1010

11-
func TestAbsent(t *testing.T) {
12-
t.Parallel()
11+
func FuzzAbsentAdversary(f *testing.F) {
12+
f.Add(98465230)
13+
f.Add(651)
14+
f.Add(-56)
15+
f.Add(22)
16+
f.Add(0)
17+
f.Fuzz(absentAdversaryTest)
18+
}
1319

14-
repeatInParallel(t, 1, func(t *testing.T, repetition int) {
15-
// Total network size of 3 + 1, where the adversary has 1/4 of power.
16-
sm, err := sim.NewSimulation(asyncOptions(t, repetition,
20+
func absentAdversaryTest(t *testing.T, latencySeed int) {
21+
sm, err := sim.NewSimulation(
22+
asyncOptions(latencySeed,
23+
// Total network size of 3 + 1, where the adversary has 1/4 of power.
1724
sim.AddHonestParticipants(
1825
3,
1926
sim.NewUniformECChainGenerator(tipSetGeneratorSeed, 1, 10),
2027
uniformOneStoragePower),
2128
sim.WithAdversary(adversary.NewAbsentGenerator(oneStoragePower)),
2229
)...)
23-
require.NoError(t, err)
24-
require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe())
25-
})
30+
require.NoError(t, err)
31+
require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe())
2632
}

test/constants_test.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package test
22

33
import (
4-
"testing"
54
"time"
65

76
"github.com/filecoin-project/go-f3/gpbft"
87
"github.com/filecoin-project/go-f3/sim"
98
"github.com/filecoin-project/go-f3/sim/latency"
10-
"github.com/stretchr/testify/require"
119
)
1210

1311
const (
@@ -16,7 +14,6 @@ const (
1614

1715
latencyAsync = 100 * time.Millisecond
1816
maxRounds = 10
19-
asyncIterations = 5000
2017
EcEpochDuration = 30 * time.Second
2118
EcStabilisationDelay = 3 * time.Second
2219
)
@@ -44,11 +41,9 @@ func syncOptions(o ...sim.Option) []sim.Option {
4441
)
4542
}
4643

47-
func asyncOptions(t *testing.T, latencySeed int, o ...sim.Option) []sim.Option {
48-
lm, err := latency.NewLogNormal(int64(latencySeed), latencyAsync)
49-
require.NoError(t, err)
44+
func asyncOptions(latencySeed int, o ...sim.Option) []sim.Option {
5045
return append(o,
51-
sim.WithLatencyModel(lm),
46+
sim.WithLatencyModel(latency.NewLogNormal(int64(latencySeed), latencyAsync)),
5247
sim.WithECEpochDuration(EcEpochDuration),
5348
sim.WitECStabilisationDelay(EcStabilisationDelay),
5449
sim.WithGpbftOptions(testGpbftOptions...),

test/decide_test.go

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package test
22

33
import (
44
"fmt"
5+
"math/rand"
56
"testing"
67

78
"github.com/filecoin-project/go-f3/gpbft"
@@ -10,19 +11,30 @@ import (
1011
"github.com/stretchr/testify/require"
1112
)
1213

13-
func TestImmediateDecide(t *testing.T) {
14-
tsg := sim.NewTipSetGenerator(984615)
14+
func FuzzImmediateDecideAdversary(f *testing.F) {
15+
f.Add(98562314)
16+
f.Add(8)
17+
f.Add(-9554)
18+
f.Add(95)
19+
f.Add(65)
20+
f.Fuzz(immediateDecideAdversaryTest)
21+
}
22+
23+
func immediateDecideAdversaryTest(t *testing.T, seed int) {
24+
rng := rand.New(rand.NewSource(int64(seed)))
25+
tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed)
1526
baseChain := generateECChain(t, tsg)
1627
adversaryValue := baseChain.Extend(tsg.Sample())
17-
sm, err := sim.NewSimulation(asyncOptions(t, 1413,
18-
sim.AddHonestParticipants(
19-
1,
20-
sim.NewUniformECChainGenerator(tipSetGeneratorSeed, 1, 10),
21-
uniformOneStoragePower),
22-
sim.WithBaseChain(&baseChain),
23-
// Add the adversary to the simulation with 3/4 of total power.
24-
sim.WithAdversary(adversary.NewImmediateDecideGenerator(adversaryValue, gpbft.NewStoragePower(3))),
25-
)...)
28+
sm, err := sim.NewSimulation(
29+
asyncOptions(rng.Int(),
30+
sim.AddHonestParticipants(
31+
1,
32+
sim.NewUniformECChainGenerator(rng.Uint64(), 1, 10),
33+
uniformOneStoragePower),
34+
sim.WithBaseChain(&baseChain),
35+
// Add the adversary to the simulation with 3/4 of total power.
36+
sim.WithAdversary(adversary.NewImmediateDecideGenerator(adversaryValue, gpbft.NewStoragePower(3))),
37+
)...)
2638
require.NoError(t, err)
2739

2840
err = sm.Run(1, maxRounds)

0 commit comments

Comments
 (0)