github.com/core-coin/go-core/v2@v2.1.9/core/rawdb/freezer_table_test.go (about)

     1  // Copyright 2019 by the Authors
     2  // This file is part of the go-core library.
     3  //
     4  // The go-core library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-core library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-core library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package rawdb
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"math/rand"
    23  	"os"
    24  	"path/filepath"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/core-coin/go-core/v2/metrics"
    29  )
    30  
    31  func init() {
    32  	rand.Seed(time.Now().Unix())
    33  }
    34  
    35  // Gets a chunk of data, filled with 'b'
    36  func getChunk(size int, b int) []byte {
    37  	data := make([]byte, size)
    38  	for i := range data {
    39  		data[i] = byte(b)
    40  	}
    41  	return data
    42  }
    43  
    44  // TestFreezerBasics test initializing a freezertable from scratch, writing to the table,
    45  // and reading it back.
    46  func TestFreezerBasics(t *testing.T) {
    47  	t.Parallel()
    48  	// set cutoff at 50 bytes
    49  	f, err := newCustomTable(os.TempDir(),
    50  		fmt.Sprintf("unittest-%d", rand.Uint64()),
    51  		metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge(), 50, true)
    52  	if err != nil {
    53  		t.Fatal(err)
    54  	}
    55  	defer f.Close()
    56  	// Write 15 bytes 255 times, results in 85 files
    57  	for x := 0; x < 255; x++ {
    58  		data := getChunk(15, x)
    59  		f.Append(uint64(x), data)
    60  	}
    61  
    62  	//print(t, f, 0)
    63  	//print(t, f, 1)
    64  	//print(t, f, 2)
    65  	//
    66  	//db[0] =  000000000000000000000000000000
    67  	//db[1] =  010101010101010101010101010101
    68  	//db[2] =  020202020202020202020202020202
    69  
    70  	for y := 0; y < 255; y++ {
    71  		exp := getChunk(15, y)
    72  		got, err := f.Retrieve(uint64(y))
    73  		if err != nil {
    74  			t.Fatal(err)
    75  		}
    76  		if !bytes.Equal(got, exp) {
    77  			t.Fatalf("test %d, got \n%x != \n%x", y, got, exp)
    78  		}
    79  	}
    80  	// Check that we cannot read too far
    81  	_, err = f.Retrieve(uint64(255))
    82  	if err != errOutOfBounds {
    83  		t.Fatal(err)
    84  	}
    85  }
    86  
    87  // TestFreezerBasicsClosing tests same as TestFreezerBasics, but also closes and reopens the freezer between
    88  // every operation
    89  func TestFreezerBasicsClosing(t *testing.T) {
    90  	t.Parallel()
    91  	// set cutoff at 50 bytes
    92  	var (
    93  		fname      = fmt.Sprintf("basics-close-%d", rand.Uint64())
    94  		rm, wm, sg = metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
    95  		f          *freezerTable
    96  		err        error
    97  	)
    98  	f, err = newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
    99  	if err != nil {
   100  		t.Fatal(err)
   101  	}
   102  	// Write 15 bytes 255 times, results in 85 files
   103  	for x := 0; x < 255; x++ {
   104  		data := getChunk(15, x)
   105  		f.Append(uint64(x), data)
   106  		f.Close()
   107  		f, err = newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   108  		if err != nil {
   109  			t.Fatal(err)
   110  		}
   111  	}
   112  	defer f.Close()
   113  
   114  	for y := 0; y < 255; y++ {
   115  		exp := getChunk(15, y)
   116  		got, err := f.Retrieve(uint64(y))
   117  		if err != nil {
   118  			t.Fatal(err)
   119  		}
   120  		if !bytes.Equal(got, exp) {
   121  			t.Fatalf("test %d, got \n%x != \n%x", y, got, exp)
   122  		}
   123  		f.Close()
   124  		f, err = newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   125  		if err != nil {
   126  			t.Fatal(err)
   127  		}
   128  	}
   129  }
   130  
   131  // TestFreezerRepairDanglingHead tests that we can recover if index entries are removed
   132  func TestFreezerRepairDanglingHead(t *testing.T) {
   133  	t.Parallel()
   134  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   135  	fname := fmt.Sprintf("dangling_headtest-%d", rand.Uint64())
   136  
   137  	{ // Fill table
   138  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   139  		if err != nil {
   140  			t.Fatal(err)
   141  		}
   142  		// Write 15 bytes 255 times
   143  		for x := 0; x < 255; x++ {
   144  			data := getChunk(15, x)
   145  			f.Append(uint64(x), data)
   146  		}
   147  		// The last item should be there
   148  		if _, err = f.Retrieve(0xfe); err != nil {
   149  			t.Fatal(err)
   150  		}
   151  		f.Close()
   152  	}
   153  	// open the index
   154  	idxFile, err := os.OpenFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s.ridx", fname)), os.O_RDWR, 0644)
   155  	if err != nil {
   156  		t.Fatalf("Failed to open index file: %v", err)
   157  	}
   158  	// Remove 4 bytes
   159  	stat, err := idxFile.Stat()
   160  	if err != nil {
   161  		t.Fatalf("Failed to stat index file: %v", err)
   162  	}
   163  	idxFile.Truncate(stat.Size() - 4)
   164  	idxFile.Close()
   165  	// Now open it again
   166  	{
   167  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   168  		if err != nil {
   169  			t.Fatal(err)
   170  		}
   171  		// The last item should be missing
   172  		if _, err = f.Retrieve(0xff); err == nil {
   173  			t.Errorf("Expected error for missing index entry")
   174  		}
   175  		// The one before should still be there
   176  		if _, err = f.Retrieve(0xfd); err != nil {
   177  			t.Fatalf("Expected no error, got %v", err)
   178  		}
   179  	}
   180  }
   181  
   182  // TestFreezerRepairDanglingHeadLarge tests that we can recover if very many index entries are removed
   183  func TestFreezerRepairDanglingHeadLarge(t *testing.T) {
   184  	t.Parallel()
   185  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   186  	fname := fmt.Sprintf("dangling_headtest-%d", rand.Uint64())
   187  
   188  	{ // Fill a table and close it
   189  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   190  		if err != nil {
   191  			t.Fatal(err)
   192  		}
   193  		// Write 15 bytes 255 times
   194  		for x := 0; x < 0xff; x++ {
   195  			data := getChunk(15, x)
   196  			f.Append(uint64(x), data)
   197  		}
   198  		// The last item should be there
   199  		if _, err = f.Retrieve(f.items - 1); err != nil {
   200  			t.Fatal(err)
   201  		}
   202  		f.Close()
   203  	}
   204  	// open the index
   205  	idxFile, err := os.OpenFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s.ridx", fname)), os.O_RDWR, 0644)
   206  	if err != nil {
   207  		t.Fatalf("Failed to open index file: %v", err)
   208  	}
   209  	// Remove everything but the first item, and leave data unaligned
   210  	// 0-indexEntry, 1-indexEntry, corrupt-indexEntry
   211  	idxFile.Truncate(indexEntrySize + indexEntrySize + indexEntrySize/2)
   212  	idxFile.Close()
   213  	// Now open it again
   214  	{
   215  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   216  		if err != nil {
   217  			t.Fatal(err)
   218  		}
   219  		// The first item should be there
   220  		if _, err = f.Retrieve(0); err != nil {
   221  			t.Fatal(err)
   222  		}
   223  		// The second item should be missing
   224  		if _, err = f.Retrieve(1); err == nil {
   225  			t.Errorf("Expected error for missing index entry")
   226  		}
   227  		// We should now be able to store items again, from item = 1
   228  		for x := 1; x < 0xff; x++ {
   229  			data := getChunk(15, ^x)
   230  			f.Append(uint64(x), data)
   231  		}
   232  		f.Close()
   233  	}
   234  	// And if we open it, we should now be able to read all of them (new values)
   235  	{
   236  		f, _ := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   237  		for y := 1; y < 255; y++ {
   238  			exp := getChunk(15, ^y)
   239  			got, err := f.Retrieve(uint64(y))
   240  			if err != nil {
   241  				t.Fatal(err)
   242  			}
   243  			if !bytes.Equal(got, exp) {
   244  				t.Fatalf("test %d, got \n%x != \n%x", y, got, exp)
   245  			}
   246  		}
   247  	}
   248  }
   249  
   250  // TestSnappyDetection tests that we fail to open a snappy database and vice versa
   251  func TestSnappyDetection(t *testing.T) {
   252  	t.Parallel()
   253  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   254  	fname := fmt.Sprintf("snappytest-%d", rand.Uint64())
   255  	// Open with snappy
   256  	{
   257  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   258  		if err != nil {
   259  			t.Fatal(err)
   260  		}
   261  		// Write 15 bytes 255 times
   262  		for x := 0; x < 0xff; x++ {
   263  			data := getChunk(15, x)
   264  			f.Append(uint64(x), data)
   265  		}
   266  		f.Close()
   267  	}
   268  	// Open without snappy
   269  	{
   270  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, false)
   271  		if err != nil {
   272  			t.Fatal(err)
   273  		}
   274  		if _, err = f.Retrieve(0); err == nil {
   275  			f.Close()
   276  			t.Fatalf("expected empty table")
   277  		}
   278  	}
   279  
   280  	// Open with snappy
   281  	{
   282  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   283  		if err != nil {
   284  			t.Fatal(err)
   285  		}
   286  		// There should be 255 items
   287  		if _, err = f.Retrieve(0xfe); err != nil {
   288  			f.Close()
   289  			t.Fatalf("expected no error, got %v", err)
   290  		}
   291  	}
   292  
   293  }
   294  func assertFileSize(f string, size int64) error {
   295  	stat, err := os.Stat(f)
   296  	if err != nil {
   297  		return err
   298  	}
   299  	if stat.Size() != size {
   300  		return fmt.Errorf("error, expected size %d, got %d", size, stat.Size())
   301  	}
   302  	return nil
   303  
   304  }
   305  
   306  // TestFreezerRepairDanglingIndex checks that if the index has more entries than there are data,
   307  // the index is repaired
   308  func TestFreezerRepairDanglingIndex(t *testing.T) {
   309  	t.Parallel()
   310  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   311  	fname := fmt.Sprintf("dangling_indextest-%d", rand.Uint64())
   312  
   313  	{ // Fill a table and close it
   314  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   315  		if err != nil {
   316  			t.Fatal(err)
   317  		}
   318  		// Write 15 bytes 9 times : 150 bytes
   319  		for x := 0; x < 9; x++ {
   320  			data := getChunk(15, x)
   321  			f.Append(uint64(x), data)
   322  		}
   323  		// The last item should be there
   324  		if _, err = f.Retrieve(f.items - 1); err != nil {
   325  			f.Close()
   326  			t.Fatal(err)
   327  		}
   328  		f.Close()
   329  		// File sizes should be 45, 45, 45 : items[3, 3, 3)
   330  	}
   331  	// Crop third file
   332  	fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.0002.rdat", fname))
   333  	// Truncate third file: 45 ,45, 20
   334  	{
   335  		if err := assertFileSize(fileToCrop, 45); err != nil {
   336  			t.Fatal(err)
   337  		}
   338  		file, err := os.OpenFile(fileToCrop, os.O_RDWR, 0644)
   339  		if err != nil {
   340  			t.Fatal(err)
   341  		}
   342  		file.Truncate(20)
   343  		file.Close()
   344  	}
   345  	// Open db it again
   346  	// It should restore the file(s) to
   347  	// 45, 45, 15
   348  	// with 3+3+1 items
   349  	{
   350  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   351  		if err != nil {
   352  			t.Fatal(err)
   353  		}
   354  		if f.items != 7 {
   355  			f.Close()
   356  			t.Fatalf("expected %d items, got %d", 7, f.items)
   357  		}
   358  		if err := assertFileSize(fileToCrop, 15); err != nil {
   359  			t.Fatal(err)
   360  		}
   361  	}
   362  }
   363  
   364  func TestFreezerTruncate(t *testing.T) {
   365  
   366  	t.Parallel()
   367  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   368  	fname := fmt.Sprintf("truncation-%d", rand.Uint64())
   369  
   370  	{ // Fill table
   371  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   372  		if err != nil {
   373  			t.Fatal(err)
   374  		}
   375  		// Write 15 bytes 30 times
   376  		for x := 0; x < 30; x++ {
   377  			data := getChunk(15, x)
   378  			f.Append(uint64(x), data)
   379  		}
   380  		// The last item should be there
   381  		if _, err = f.Retrieve(f.items - 1); err != nil {
   382  			t.Fatal(err)
   383  		}
   384  		f.Close()
   385  	}
   386  	// Reopen, truncate
   387  	{
   388  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   389  		if err != nil {
   390  			t.Fatal(err)
   391  		}
   392  		defer f.Close()
   393  		f.truncate(10) // 150 bytes
   394  		if f.items != 10 {
   395  			t.Fatalf("expected %d items, got %d", 10, f.items)
   396  		}
   397  		// 45, 45, 45, 15 -- bytes should be 15
   398  		if f.headBytes != 15 {
   399  			t.Fatalf("expected %d bytes, got %d", 15, f.headBytes)
   400  		}
   401  
   402  	}
   403  
   404  }
   405  
   406  // TestFreezerRepairFirstFile tests a head file with the very first item only half-written.
   407  // That will rewind the index, and _should_ truncate the head file
   408  func TestFreezerRepairFirstFile(t *testing.T) {
   409  	t.Parallel()
   410  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   411  	fname := fmt.Sprintf("truncationfirst-%d", rand.Uint64())
   412  	{ // Fill table
   413  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   414  		if err != nil {
   415  			t.Fatal(err)
   416  		}
   417  		// Write 80 bytes, splitting out into two files
   418  		f.Append(0, getChunk(40, 0xFF))
   419  		f.Append(1, getChunk(40, 0xEE))
   420  		// The last item should be there
   421  		if _, err = f.Retrieve(f.items - 1); err != nil {
   422  			t.Fatal(err)
   423  		}
   424  		f.Close()
   425  	}
   426  	// Truncate the file in half
   427  	fileToCrop := filepath.Join(os.TempDir(), fmt.Sprintf("%s.0001.rdat", fname))
   428  	{
   429  		if err := assertFileSize(fileToCrop, 40); err != nil {
   430  			t.Fatal(err)
   431  		}
   432  		file, err := os.OpenFile(fileToCrop, os.O_RDWR, 0644)
   433  		if err != nil {
   434  			t.Fatal(err)
   435  		}
   436  		file.Truncate(20)
   437  		file.Close()
   438  	}
   439  	// Reopen
   440  	{
   441  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   442  		if err != nil {
   443  			t.Fatal(err)
   444  		}
   445  		if f.items != 1 {
   446  			f.Close()
   447  			t.Fatalf("expected %d items, got %d", 0, f.items)
   448  		}
   449  		// Write 40 bytes
   450  		f.Append(1, getChunk(40, 0xDD))
   451  		f.Close()
   452  		// Should have been truncated down to zero and then 40 written
   453  		if err := assertFileSize(fileToCrop, 40); err != nil {
   454  			t.Fatal(err)
   455  		}
   456  	}
   457  }
   458  
   459  // TestFreezerReadAndTruncate tests:
   460  // - we have a table open
   461  // - do some reads, so files are open in readonly
   462  // - truncate so those files are 'removed'
   463  // - check that we did not keep the rdonly file descriptors
   464  func TestFreezerReadAndTruncate(t *testing.T) {
   465  	t.Parallel()
   466  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   467  	fname := fmt.Sprintf("read_truncate-%d", rand.Uint64())
   468  	{ // Fill table
   469  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   470  		if err != nil {
   471  			t.Fatal(err)
   472  		}
   473  		// Write 15 bytes 30 times
   474  		for x := 0; x < 30; x++ {
   475  			data := getChunk(15, x)
   476  			f.Append(uint64(x), data)
   477  		}
   478  		// The last item should be there
   479  		if _, err = f.Retrieve(f.items - 1); err != nil {
   480  			t.Fatal(err)
   481  		}
   482  		f.Close()
   483  	}
   484  	// Reopen and read all files
   485  	{
   486  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 50, true)
   487  		if err != nil {
   488  			t.Fatal(err)
   489  		}
   490  		if f.items != 30 {
   491  			f.Close()
   492  			t.Fatalf("expected %d items, got %d", 0, f.items)
   493  		}
   494  		for y := byte(0); y < 30; y++ {
   495  			f.Retrieve(uint64(y))
   496  		}
   497  		// Now, truncate back to zero
   498  		f.truncate(0)
   499  		// Write the data again
   500  		for x := 0; x < 30; x++ {
   501  			data := getChunk(15, ^x)
   502  			if err := f.Append(uint64(x), data); err != nil {
   503  				t.Fatalf("error %v", err)
   504  			}
   505  		}
   506  		f.Close()
   507  	}
   508  }
   509  
   510  func TestOffset(t *testing.T) {
   511  	t.Parallel()
   512  	rm, wm, sg := metrics.NewMeter(), metrics.NewMeter(), metrics.NewGauge()
   513  	fname := fmt.Sprintf("offset-%d", rand.Uint64())
   514  	{ // Fill table
   515  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 40, true)
   516  		if err != nil {
   517  			t.Fatal(err)
   518  		}
   519  		// Write 6 x 20 bytes, splitting out into three files
   520  		f.Append(0, getChunk(20, 0xFF))
   521  		f.Append(1, getChunk(20, 0xEE))
   522  
   523  		f.Append(2, getChunk(20, 0xdd))
   524  		f.Append(3, getChunk(20, 0xcc))
   525  
   526  		f.Append(4, getChunk(20, 0xbb))
   527  		f.Append(5, getChunk(20, 0xaa))
   528  		f.printIndex()
   529  		f.Close()
   530  	}
   531  	// Now crop it.
   532  	{
   533  		// delete files 0 and 1
   534  		for i := 0; i < 2; i++ {
   535  			p := filepath.Join(os.TempDir(), fmt.Sprintf("%v.%04d.rdat", fname, i))
   536  			if err := os.Remove(p); err != nil {
   537  				t.Fatal(err)
   538  			}
   539  		}
   540  		// Read the index file
   541  		p := filepath.Join(os.TempDir(), fmt.Sprintf("%v.ridx", fname))
   542  		indexFile, err := os.OpenFile(p, os.O_RDWR, 0644)
   543  		if err != nil {
   544  			t.Fatal(err)
   545  		}
   546  		indexBuf := make([]byte, 7*indexEntrySize)
   547  		indexFile.Read(indexBuf)
   548  
   549  		// Update the index file, so that we store
   550  		// [ file = 2, offset = 4 ] at index zero
   551  
   552  		tailId := uint32(2)     // First file is 2
   553  		itemOffset := uint32(4) // We have removed four items
   554  		zeroIndex := indexEntry{
   555  			filenum: tailId,
   556  			offset:  itemOffset,
   557  		}
   558  		buf := zeroIndex.marshallBinary()
   559  		// Overwrite index zero
   560  		copy(indexBuf, buf)
   561  		// Remove the four next indices by overwriting
   562  		copy(indexBuf[indexEntrySize:], indexBuf[indexEntrySize*5:])
   563  		indexFile.WriteAt(indexBuf, 0)
   564  		// Need to truncate the moved index items
   565  		indexFile.Truncate(indexEntrySize * (1 + 2))
   566  		indexFile.Close()
   567  
   568  	}
   569  	// Now open again
   570  	checkPresent := func(numDeleted uint64) {
   571  		f, err := newCustomTable(os.TempDir(), fname, rm, wm, sg, 40, true)
   572  		if err != nil {
   573  			t.Fatal(err)
   574  		}
   575  		f.printIndex()
   576  		// It should allow writing item 6
   577  		f.Append(numDeleted+2, getChunk(20, 0x99))
   578  
   579  		// It should be fine to fetch 4,5,6
   580  		if got, err := f.Retrieve(numDeleted); err != nil {
   581  			t.Fatal(err)
   582  		} else if exp := getChunk(20, 0xbb); !bytes.Equal(got, exp) {
   583  			t.Fatalf("expected %x got %x", exp, got)
   584  		}
   585  		if got, err := f.Retrieve(numDeleted + 1); err != nil {
   586  			t.Fatal(err)
   587  		} else if exp := getChunk(20, 0xaa); !bytes.Equal(got, exp) {
   588  			t.Fatalf("expected %x got %x", exp, got)
   589  		}
   590  		if got, err := f.Retrieve(numDeleted + 2); err != nil {
   591  			t.Fatal(err)
   592  		} else if exp := getChunk(20, 0x99); !bytes.Equal(got, exp) {
   593  			t.Fatalf("expected %x got %x", exp, got)
   594  		}
   595  
   596  		// It should error at 0, 1,2,3
   597  		for i := numDeleted - 1; i > numDeleted-10; i-- {
   598  			if _, err := f.Retrieve(i); err == nil {
   599  				t.Fatal("expected err")
   600  			}
   601  		}
   602  	}
   603  	checkPresent(4)
   604  	// Now, let's pretend we have deleted 1M items
   605  	{
   606  		// Read the index file
   607  		p := filepath.Join(os.TempDir(), fmt.Sprintf("%v.ridx", fname))
   608  		indexFile, err := os.OpenFile(p, os.O_RDWR, 0644)
   609  		if err != nil {
   610  			t.Fatal(err)
   611  		}
   612  		indexBuf := make([]byte, 3*indexEntrySize)
   613  		indexFile.Read(indexBuf)
   614  
   615  		// Update the index file, so that we store
   616  		// [ file = 2, offset = 1M ] at index zero
   617  
   618  		tailId := uint32(2)           // First file is 2
   619  		itemOffset := uint32(1000000) // We have removed 1M items
   620  		zeroIndex := indexEntry{
   621  			offset:  itemOffset,
   622  			filenum: tailId,
   623  		}
   624  		buf := zeroIndex.marshallBinary()
   625  		// Overwrite index zero
   626  		copy(indexBuf, buf)
   627  		indexFile.WriteAt(indexBuf, 0)
   628  		indexFile.Close()
   629  	}
   630  	checkPresent(1000000)
   631  }
   632  
   633  // TODO (?)
   634  // - test that if we remove several head-files, aswell as data last data-file,
   635  //   the index is truncated accordingly
   636  // Right now, the freezer would fail on these conditions:
   637  // 1. have data files d0, d1, d2, d3
   638  // 2. remove d2,d3
   639  //
   640  // However, all 'normal' failure modes arising due to failing to sync() or save a file should be
   641  // handled already, and the case described above can only (?) happen if an external process/user
   642  // deletes files from the filesystem.