Skip to content

Commit c0938d9

Browse files
committed
add test check page
Signed-off-by: Mustafa Elbehery <melbeher@redhat.com>
1 parent 6383011 commit c0938d9

File tree

1 file changed

+195
-0
lines changed

1 file changed

+195
-0
lines changed

db_whitebox_test.go

+195
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
package bbolt
22

33
import (
4+
"bytes"
5+
crand "crypto/rand"
6+
"encoding/binary"
7+
"fmt"
8+
"math/rand"
49
"path/filepath"
510
"testing"
611

712
"github.com/stretchr/testify/assert"
813
"github.com/stretchr/testify/require"
914

1015
"go.etcd.io/bbolt/errors"
16+
"go.etcd.io/bbolt/internal/common"
17+
"go.etcd.io/bbolt/internal/guts_cli"
1118
)
1219

1320
func TestOpenWithPreLoadFreelist(t *testing.T) {
@@ -112,6 +119,194 @@ func TestMethodPage(t *testing.T) {
112119
}
113120
}
114121

122+
func TestTx_Check_CorruptPage(t *testing.T) {
123+
testCases := []struct {
124+
name string
125+
bucketKey string
126+
isPanic bool
127+
isErr bool
128+
}{
129+
{
130+
name: "no corrupted page",
131+
bucketKey: "testBucket",
132+
isPanic: false,
133+
isErr: false,
134+
},
135+
{
136+
name: "corrupt page by violating btree invariant",
137+
bucketKey: "testBucket",
138+
isPanic: false,
139+
isErr: true,
140+
},
141+
{
142+
name: "corrupt page by dumping random bytes",
143+
bucketKey: "testBucket",
144+
isPanic: true,
145+
isErr: false,
146+
},
147+
}
148+
149+
for _, tc := range testCases {
150+
t.Run(tc.name, func(t *testing.T) {
151+
fileName, err := prepareData(t)
152+
require.NoError(t, err)
153+
154+
t.Logf("Creating db file '%v'.", fileName)
155+
db, err := Open(fileName, 0666, &Options{PageSize: 4096})
156+
require.NoError(t, err)
157+
defer func() {
158+
require.NoError(t, db.Close())
159+
}()
160+
161+
uErr := db.Update(func(tx *Tx) error {
162+
t.Logf("Creating bucket '%v'.", tc.bucketKey)
163+
b, bErr := tx.CreateBucketIfNotExists([]byte(tc.bucketKey))
164+
require.NoError(t, bErr)
165+
t.Logf("Generating random data in bucket '%v'.", tc.bucketKey)
166+
generateSampleDataInBucket(t, b, 3)
167+
return nil
168+
})
169+
require.NoError(t, uErr)
170+
171+
var victimPageId common.Pgid
172+
if tc.isPanic || tc.isErr {
173+
t.Logf("Corrupting random leaf page in bucket '%v'.", tc.bucketKey)
174+
victimPageId = corruptLeafPage(t, db, tc.isPanic)
175+
}
176+
177+
if tc.isPanic {
178+
defer func() {
179+
r := recover()
180+
require.NotNil(t, r)
181+
}()
182+
}
183+
184+
t.Log("Running consistency check.")
185+
vErr := db.View(func(tx *Tx) error {
186+
chkConfig := checkConfig{
187+
kvStringer: HexKVStringer(),
188+
}
189+
ch := make(chan error)
190+
defer close(ch)
191+
192+
var cErrs []error
193+
go func() {
194+
for cErr := range ch {
195+
cErrs = append(cErrs, cErr)
196+
}
197+
}()
198+
tx.check(chkConfig, ch)
199+
200+
if tc.isErr {
201+
require.Equal(t, 1, len(cErrs))
202+
require.ErrorContains(t, cErrs[0], fmt.Sprintf("leaf page(%d)", victimPageId))
203+
}
204+
return nil
205+
})
206+
require.NoError(t, vErr)
207+
})
208+
}
209+
}
210+
211+
// corruptLeafPage write an invalid leafPageElement into the victim page.
212+
func corruptLeafPage(t testing.TB, db *DB, isPanic bool) common.Pgid {
213+
t.Helper()
214+
215+
victimPageId := findVictimPageId(t, db)
216+
217+
victimPage, victimBuf, err := guts_cli.ReadPage(db.Path(), uint64(victimPageId))
218+
require.NoError(t, err)
219+
require.NotNil(t, victimPage)
220+
require.NotNil(t, victimBuf)
221+
require.True(t, victimPage.IsLeafPage())
222+
require.True(t, victimPage.Count() > 0)
223+
224+
// Dumping random bytes in victim page for corruption.
225+
copy(victimBuf[16:], generateCorruptionBytes(t, isPanic))
226+
// Write the corrupt page to db file.
227+
err = guts_cli.WritePage(db.Path(), victimBuf)
228+
require.NoError(t, err)
229+
230+
return victimPageId
231+
}
232+
233+
// findVictimPageId finds all the leaf pages of a bucket and picks a random leaf page as a victim to be corrupted.
234+
func findVictimPageId(t testing.TB, db *DB) common.Pgid {
235+
t.Helper()
236+
// Read DB's RootPage.
237+
rootPageId, _, err := guts_cli.GetRootPage(db.Path())
238+
require.NoError(t, err)
239+
rootPage, _, err := guts_cli.ReadPage(db.Path(), uint64(rootPageId))
240+
require.NoError(t, err)
241+
require.True(t, rootPage.IsLeafPage())
242+
require.Equal(t, 1, len(rootPage.LeafPageElements()))
243+
// Find Bucket's RootPage.
244+
lpe := rootPage.LeafPageElement(uint16(0))
245+
require.Equal(t, uint32(common.BranchPageFlag), lpe.Flags())
246+
k := lpe.Key()
247+
require.Equal(t, "testBucket", string(k))
248+
bucketRootPageId := lpe.Bucket().RootPage()
249+
// Read Bucket's RootPage.
250+
bucketRootPage, _, err := guts_cli.ReadPage(db.Path(), uint64(bucketRootPageId))
251+
require.NoError(t, err)
252+
require.Equal(t, uint16(common.BranchPageFlag), bucketRootPage.Flags())
253+
// Retrieve Bucket's PageIds
254+
var bucketPageIds []common.Pgid
255+
for _, bpe := range bucketRootPage.BranchPageElements() {
256+
bucketPageIds = append(bucketPageIds, bpe.Pgid())
257+
}
258+
259+
victimPageId := bucketPageIds[rand.Intn(len(bucketPageIds))]
260+
return victimPageId
261+
}
262+
263+
// generateSampleDataInBucket fill in sample data into given bucket to create the given
264+
// number of leafPages. To control the number of leafPages, sample data are generated in order.
265+
func generateSampleDataInBucket(t testing.TB, bk *Bucket, lPages int) {
266+
t.Helper()
267+
currentKey := 1
268+
currentVal := 100
269+
for i := 0; i < lPages; i++ {
270+
currentSize := common.PageHeaderSize
271+
for {
272+
err := bk.Put(convertIntIntoBytes(t, currentKey), convertIntIntoBytes(t, currentVal))
273+
require.NoError(t, err)
274+
currentSize += 16 + 4 + 4
275+
if currentSize >= 4096 {
276+
break
277+
}
278+
currentKey++
279+
currentVal++
280+
}
281+
}
282+
}
283+
284+
// generateCorruptionBytes returns random bytes to corrupt a page.
285+
// It inserts a page element which violates the btree key order if no panic is expected.
286+
// Otherwise, it dumps random bytes into a page which causes a panic.
287+
func generateCorruptionBytes(t testing.TB, isPanic bool) []byte {
288+
if isPanic {
289+
// Generated data size is between pageHeader and pageSize.
290+
corruptDataLength := rand.Intn(4096-16) + 16
291+
corruptData := make([]byte, corruptDataLength)
292+
_, err := crand.Read(corruptData)
293+
require.NoError(t, err)
294+
return corruptData
295+
}
296+
// Insert LeafPageElement which violates the BTree range.
297+
invalidLPE := common.NewLeafPageElement(0, 0, 0, 0)
298+
var buf bytes.Buffer
299+
binary.Write(&buf, binary.BigEndian, invalidLPE)
300+
return buf.Bytes()
301+
}
302+
303+
func convertIntIntoBytes(t testing.TB, i int) []byte {
304+
t.Helper()
305+
buf := make([]byte, 4)
306+
binary.BigEndian.PutUint32(buf, uint32(i))
307+
return buf
308+
}
309+
115310
func prepareData(t *testing.T) (string, error) {
116311
fileName := filepath.Join(t.TempDir(), "db")
117312
db, err := Open(fileName, 0666, nil)

0 commit comments

Comments
 (0)