diff --git a/log/log_test.go b/log/log_test.go new file mode 100644 index 0000000..abd7a53 --- /dev/null +++ b/log/log_test.go @@ -0,0 +1,157 @@ +package log + +import ( + "awesomeDB/kfile" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestNewLogMgr(t *testing.T) { + // Setup + tempDir := filepath.Join(os.TempDir(), "simpledb_test_"+time.Now().Format("20060102150405")) + blockSize := 400 + fm, err := kfile.NewFileMgr(tempDir, blockSize) + if err != nil { + t.Fatalf("Failed to create FileMgr: %v", err) + } + defer func() { + fm.Close() + os.RemoveAll(tempDir) + }() + + filename := "new_log.db" + logMgr, err := newLogMgr(fm, filename) + if err != nil { + t.Fatalf("Failed to create LogMgr for new log file: %v", err) + } + if logMgr.logsize != 0 { + t.Errorf("Expected logsize 0 for new log file, got %d", logMgr.logsize) + } + + // Test for an existing log file + logMgr.Append([]byte("test record")) + logMgr.Flush() + + logMgr2, err := newLogMgr(fm, filename) + if err != nil { + t.Fatalf("Failed to create LogMgr for existing log file: %v", err) + } + if logMgr2.logsize == 0 { + t.Errorf("Expected logsize > 0 for existing log file, got %d", logMgr2.logsize) + } +} + +func TestAppend(t *testing.T) { + // Setup + tempDir := filepath.Join(os.TempDir(), "simpledb_test_"+time.Now().Format("20060102150405")) + var blockSize int = 400 + fm, err := kfile.NewFileMgr(tempDir, blockSize) + if err != nil { + t.Fatalf("Failed to create FileMgr: %v", err) + } + defer func() { + fm.Close() + os.RemoveAll(tempDir) + }() + + logMgr, err := newLogMgr(fm, "append_test.db") + if err != nil { + t.Fatalf("Failed to initialize LogMgr: %v", err) + } + + // Append records and check LSN + record := []byte("test record") + for i := 0; i < 10; i++ { + lsn := logMgr.Append(record) + if lsn != i+1 { + t.Errorf("Expected LSN %d, got %d", i+1, lsn) + } + } + + // Verify boundary updates correctly + boundary, _ := logMgr.logPage.GetInt(0) + if boundary <= 0 || boundary >= blockSize { + t.Errorf("Invalid boundary after append: %d", boundary) + } +} + +func TestFlush(t *testing.T) { + // Setup + tempDir := filepath.Join(os.TempDir(), "simpledb_test_"+time.Now().Format("20060102150405")) + blockSize := 400 + fm, err := kfile.NewFileMgr(tempDir, blockSize) + if err != nil { + t.Fatalf("Failed to create FileMgr: %v", err) + } + defer func() { + fm.Close() + os.RemoveAll(tempDir) + }() + + logMgr, err := newLogMgr(fm, "flush_test.db") + if err != nil { + t.Fatalf("Failed to initialize LogMgr: %v", err) + } + + // Append a record + record := []byte("flush record") + logMgr.Append(record) + + // Flush and verify + err = logMgr.Flush() + if err != nil { + t.Fatalf("Flush failed: %v", err) + } + + // Read the block to confirm data was written + page := kfile.NewPage(blockSize) + err = fm.Read(logMgr.currentBlock, page) + if err != nil { + t.Fatalf("Failed to read block after flush: %v", err) + } + recpos, err := logMgr.logPage.GetInt(0) + if err != nil { + t.Errorf("Error getting recpos %s", err) + } + readRecord, _ := page.GetBytes(int(recpos)) + readRecordStr := string(readRecord) + readRecordStr = strings.TrimRight(readRecordStr, "\x00 ") // Trim nulls and spacesZZ + if string(readRecordStr) != string(record) { + t.Errorf("Expected record '%s', got '%s'", string(record), string(readRecord)) + } +} + +func TestAppendBoundary(t *testing.T) { + // Setup + tempDir := filepath.Join(os.TempDir(), "simpledb_test_"+time.Now().Format("20060102150405")) + blockSize := 400 + fm, err := kfile.NewFileMgr(tempDir, blockSize) + if err != nil { + t.Fatalf("Failed to create FileMgr: %v", err) + } + defer func() { + fm.Close() + os.RemoveAll(tempDir) + }() + + logMgr, err := newLogMgr(fm, "boundary_test.db") + if err != nil { + t.Fatalf("Failed to initialize LogMgr: %v", err) + } + + // Append records to fill the block + record := make([]byte, 50) // Record size + for i := 0; i < blockSize/len(record)-1; i++ { + logMgr.Append(record) + } + + initialBlock := logMgr.currentBlock + logMgr.Append(record) + + if logMgr.currentBlock == initialBlock { + t.Errorf("Expected new block after boundary overflow, but block did not change") + } +} diff --git a/log/logmgr_test.go b/log/logmgr_test.go new file mode 100644 index 0000000..b1f0aab --- /dev/null +++ b/log/logmgr_test.go @@ -0,0 +1,92 @@ +package log + +// +//import ( +// "awesomeDB/kfile" +// "awesomeDB/utils" +// "fmt" +// "os" +// "path/filepath" +// "testing" +// "time" +// "unsafe" +//) +// +//func TestLogMgr(t *testing.T) { +// // Setup +// tempDir := filepath.Join(os.TempDir(), "simpledb_test_"+time.Now().Format("20060102150405")) +// blockSize := 400 +// fm, err := kfile.NewFileMgr(tempDir, blockSize) +// if err != nil { +// t.Fatalf("Failed to create FileMgr: %v", err) +// } +// defer func() { +// fm.Close() +// os.RemoveAll(tempDir) +// }() +// +// // Test file creation and appending +// filename := "test.db" +// _, err = fm.Append(filename) +// if err != nil { +// t.Fatalf("Failed to append block: %v", err) +// } +// lm, _ := newLogMgr(fm, filename) +// +// createRecords(t, lm, 1, 35) +// printLogRecords(t, lm, "The log file now has these records:") +// +// // Create and append additional records +// createRecords(t, lm, 36, 70) +// err = lm.Flush() +// if err != nil { +// return +// } +// printLogRecords(t, lm, "The log file now has these records:") +//} +// +//func createRecords(t *testing.T, lm *LogMgr, start, end int) { +// t.Logf("Creating records:") +// for i := start; i <= end; i++ { +// record := createLogRecord(fmt.Sprintf("record%d", i), i+100) +// lsn := lm.Append(record) +// t.Logf("Record LSN: %d", lsn) +// } +//} +// +//func printLogRecords(t *testing.T, lm *LogMgr, msg string) { +// t.Log(msg) +// iter := lm.Iterator() +// for iter.HasNext() { +// rec, err := iter.Next() +// if err != nil { +// panic(err) +// } +// page := kfile.NewPageFromBytes(rec) +// s, err := page.GetString(0) +// if err != nil { +// panic(err) +// } +// npos := utils.MaxLength(len(s)) +// val, _ := page.GetInt(npos) +// t.Logf("[%s, %d]", s, val) +// } +// t.Log() +//} +// +//func createLogRecord(s string, n int) []byte { +// npos := utils.MaxLength(len(s)) +// record := make([]byte, npos+int(unsafe.Sizeof(0))) // String + Integer +// page := kfile.NewPageFromBytes(record) +// +// if err := page.SetString(0, s); err != nil { +// panic(fmt.Sprintf("Failed to set string: %v", err)) +// } +// if err := page.SetInt(npos, n); err != nil { +// panic(fmt.Sprintf("Failed to set int: %v", err)) +// } +// +// // Log serialized record details +// fmt.Printf("Serialized record [%s, %d]: npos=%d, recordLen=%d\n", s, n, npos, len(record)) +// return record +//} diff --git a/log/temp_test.go b/log/temp_test.go new file mode 100644 index 0000000..f6ac2df --- /dev/null +++ b/log/temp_test.go @@ -0,0 +1,139 @@ +package log + +import ( + "awesomeDB/kfile" + "awesomeDB/utils" + "fmt" + "os" + "path/filepath" + "testing" + "time" +) + +func TestLogMgrAppend(t *testing.T) { + // Setup + tempDir := filepath.Join(os.TempDir(), "logmgr_test_"+time.Now().Format("20060102150405")) + blockSize := 400 + fm, err := kfile.NewFileMgr(tempDir, blockSize) + if err != nil { + t.Fatalf("Failed to create FileMgr: %v", err) + } + defer func() { + fm.Close() + os.RemoveAll(tempDir) + }() + + filename := "test.db" + _, err = fm.Append(filename) + if err != nil { + t.Fatalf("Failed to append block: %v", err) + } + lm, err := newLogMgr(fm, filename) + if err != nil { + t.Fatalf("Failed to initialize LogMgr: %v", err) + } + + // Test cases + t.Run("AppendMultipleRecordsWithinSingleBlock", func(t *testing.T) { + verifyMultipleRecordsInSingleBlock(t, lm, blockSize) + }) + + t.Run("AppendRecordsAcrossMultipleBlocks", func(t *testing.T) { + verifyRecordsAcrossBlocks(t, lm, blockSize) + }) +} + +func verifyMultipleRecordsInSingleBlock(t *testing.T, lm *LogMgr, blockSize int) { + t.Log("Testing appending multiple records within a single block...") + + // Append multiple small records + record1 := createLogRecord("record1", 100) + record2 := createLogRecord("record2", 200) + + lsn1 := lm.Append(record1) + lsn2 := lm.Append(record2) + + // Assert LSNs + if lsn1 != 1 || lsn2 != 2 { + t.Errorf("Expected LSNs 1 and 2, got %d and %d", lsn1, lsn2) + } + + // Read back records to verify correctness + iter := lm.Iterator() + records := readAllRecords(t, iter) + expected := []string{"record1, 100", "record2, 200"} + compareRecords(t, records, expected) +} + +func verifyRecordsAcrossBlocks(t *testing.T, lm *LogMgr, blockSize int) { + t.Log("Testing appending records across multiple blocks...") + + // Append enough records to exceed block size + // Each record is 1/5 of the block + records := []string{} + for i := 1; i <= 10; i++ { + record := createLogRecord(fmt.Sprintf("record%d", i), i*10) + lm.Append(record) + records = append(records, fmt.Sprintf("record%d, %d", i, i*10)) + } + + // Verify all records + iter := lm.Iterator() + readRecords := readAllRecords(t, iter) + compareRecords(t, readRecords, records) +} + +func createLogRecord(s string, n int) []byte { + strOffset := utils.MaxLength(len(s)) + record := make([]byte, strOffset+4) // String + Integer + page := kfile.NewPageFromBytes(record) + + if err := page.SetString(0, s); err != nil { + panic(fmt.Sprintf("Failed to set string: %v", err)) + } + if err := page.SetInt(strOffset, n); err != nil { + panic(fmt.Sprintf("Failed to set int: %v", err)) + } + + // Log serialized record details + fmt.Printf("Serialized record [%s, %d]: strOffset=%d, recordLen=%d\n", s, n, strOffset, len(record)) + return record +} + +func readAllRecords(t *testing.T, iter utils.Iterator[[]byte]) []string { + var records []string + for iter.HasNext() { + rec, err := iter.Next() + if err != nil { + t.Fatalf("Error reading record: %v", err) + } + + page := kfile.NewPageFromBytes(rec) + s, err := page.GetString(0) + if err != nil { + t.Fatalf("Error getting string: %v", err) + } + + npos := utils.MaxLength(len(s)) // Ensure alignment + n, err := page.GetInt(npos) + if err != nil { + t.Fatalf("Error getting int: %v", err) + } + + record := fmt.Sprintf("%s, %d", s, n) + records = append(records, record) + t.Logf("Deserialized record: [%s, %d] (npos=%d, recLen=%d)", s, n, npos, len(rec)) + } + return records +} + +func compareRecords(t *testing.T, actual, expected []string) { + if len(actual) != len(expected) { + t.Errorf("Expected %d records, but got %d", len(expected), len(actual)) + } + for i, rec := range actual { + if rec != expected[i] { + t.Errorf("Expected record %d to be %q, but got %q", i+1, expected[i], rec) + } + } +} diff --git a/utils/logIterator_test.go b/utils/logIterator_test.go new file mode 100644 index 0000000..8234c45 --- /dev/null +++ b/utils/logIterator_test.go @@ -0,0 +1,304 @@ +package utils + +import ( + "awesomeDB/kfile" + "os" + "path/filepath" + _ "path/filepath" + "testing" + "time" + "unsafe" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Helper function to create a temporary file manager +func createTempFileMgr(t *testing.T) *kfile.FileMgr { + tempDir, err := os.MkdirTemp("", "logiterator-test-") + require.NoError(t, err) + + t.Cleanup(func() { + os.RemoveAll(tempDir) + }) + + // Assuming FileMgr constructor takes directory and block size + fm, err := kfile.NewFileMgr(tempDir, 512) + require.NoError(t, err) + return fm +} + +// Helper function to prepare a log block with test records +func prepareLogBlock(t *testing.T, fm *kfile.FileMgr, filename string, records [][]byte) *kfile.BlockId { + blk := kfile.NewBlockId(filename, 0) + page := kfile.NewPageFromBytes(records[0]) + + // Start with boundary at the start of the page + page.SetInt(0, int(4)) // 4 is size of int32 boundary + currentPos := 4 + + for _, rec := range records { + err := page.SetBytes(currentPos, rec) + require.NoError(t, err) + + // Update position (4 bytes for integer size + record length) + currentPos += int(unsafe.Sizeof(int(0))) + len(rec) + + // Update boundary + page.SetInt(0, int(currentPos)) + } + + // Write the page to block + err := fm.Write(blk, page) + require.NoError(t, err) + + return blk +} + +func TestLogIterator_SingleBlockSingleRecord(t *testing.T) { + fm := createTempFileMgr(t) + filename := "test_single_record.log" + + testRecords := [][]byte{ + []byte("hello world"), + } + + blk := prepareLogBlock(t, fm, filename, testRecords) + + iterator := NewLogIterator(fm, blk) + + // Verify HasNext + assert.True(t, iterator.HasNext()) + + // Verify Next retrieves correct record + retrievedRec, _ := iterator.Next() + assert.Equal(t, testRecords[0], retrievedRec) + + // Verify no more records + assert.False(t, iterator.HasNext()) +} + +func TestLogIterator_MultipleRecordsSameBlock(t *testing.T) { + fm := createTempFileMgr(t) + filename := "test_multiple_records.log" + + testRecords := [][]byte{ + []byte("first record"), + []byte("second record"), + []byte("third record"), + } + + blk := prepareLogBlock(t, fm, filename, testRecords) + + iterator := NewLogIterator(fm, blk) + + // Verify records are retrieved in reverse order + for i := len(testRecords) - 1; i >= 0; i-- { + assert.True(t, iterator.HasNext()) + retrievedRec, _ := iterator.Next() + assert.Equal(t, testRecords[i], retrievedRec) + } + + assert.False(t, iterator.HasNext()) +} + +func TestLogIterator_MultipleBlocks(t *testing.T) { + fm := createTempFileMgr(t) + filename := "test_multiple_blocks.log" + + // Prepare first block + firstBlockRecords := [][]byte{ + []byte("first block first record"), + []byte("first block second record"), + } + firstBlk := prepareLogBlock(t, fm, filename, firstBlockRecords) + + // Prepare second block + secondBlockRecords := [][]byte{ + []byte("second block first record"), + []byte("second block second record"), + } + secondBlk := kfile.NewBlockId(filename, 1) + secondPage := kfile.NewPage(fm.BlockSize()) + secondPage.SetInt(0, int(4)) + + currentPos := 4 + for _, rec := range secondBlockRecords { + err := secondPage.SetBytes(currentPos, rec) + require.NoError(t, err) + + currentPos += int(unsafe.Sizeof(int(0))) + len(rec) + secondPage.SetInt(0, int(currentPos)) + } + + err := fm.Write(secondBlk, secondPage) + require.NoError(t, err) + + // Create iterator starting from first block + iterator := NewLogIterator(fm, firstBlk) + + // Expected records in reverse order + expectedRecords := append(firstBlockRecords, secondBlockRecords...) + + // Verify records retrieval across blocks + for i := len(expectedRecords) - 1; i >= 0; i-- { + assert.True(t, iterator.HasNext()) + retrievedRec, _ := iterator.Next() + assert.Equal(t, expectedRecords[i], retrievedRec) + } + + assert.False(t, iterator.HasNext()) +} + +func TestLogIterator_EmptyIterator(t *testing.T) { + fm := createTempFileMgr(t) + filename := "test_empty.log" + + // Create an empty block + blk := kfile.NewBlockId(filename, 0) + page := kfile.NewPage(fm.BlockSize()) + page.SetInt(0, int(4)) // Set boundary to start of page + + err := fm.Write(blk, page) + require.NoError(t, err) + + iterator := NewLogIterator(fm, blk) + + assert.False(t, iterator.HasNext()) +} + +func TestLogIterator_NilFileMgr(t *testing.T) { + iterator := NewLogIterator(nil, nil) + + assert.False(t, iterator.HasNext()) +} + +func setupTestFileMgr(t *testing.T) (*kfile.FileMgr, string) { + tempDir := filepath.Join(os.TempDir(), "simpledb_test_"+time.Now().Format("20060102150405")) + blockSize := 400 + fm, err := kfile.NewFileMgr(tempDir, blockSize) + if err != nil { + t.Fatalf("Failed to create FileMgr: %v", err) + } + return fm, tempDir +} + +func TestMoveToBlock(t *testing.T) { + fm, tempDir := setupTestFileMgr(t) + defer func() { + fm.Close() + os.RemoveAll(tempDir) + }() + + filename := "test.db" + block := kfile.NewBlockId(filename, 0) + page := kfile.NewPage(fm.BlockSize()) + + // Write a boundary value to the page + page.SetInt(0, 200) // Arbitrary boundary position + err := fm.Write(block, page) + if err != nil { + t.Fatalf("Failed to write block: %v", err) + } + + // Initialize LogIterator and move to block + iter := &LogIterator{fm: fm, blk: block, p: kfile.NewPage(fm.BlockSize())} + iter.moveToBlock(block) + + if iter.boundary != 200 { + t.Errorf("Expected boundary to be 200, got %d", iter.boundary) + } + if iter.currentPos != 200 { + t.Errorf("Expected currentPos to be 200, got %d", iter.currentPos) + } +} + +func TestHasNext(t *testing.T) { + fm, tempDir := setupTestFileMgr(t) + defer func() { + fm.Close() + os.RemoveAll(tempDir) + }() + + filename := "test.db" + block := kfile.NewBlockId(filename, 0) + page := kfile.NewPage(fm.BlockSize()) + + // Write a boundary value to simulate data + page.SetInt(0, 200) + err := fm.Write(block, page) + if err != nil { + t.Fatalf("Failed to write block: %v", err) + } + + // Initialize LogIterator + iter := NewLogIterator(fm, block) + + // Check `HasNext` when there is data + if !iter.HasNext() { + t.Errorf("Expected HasNext to be true, got false") + } + + // Simulate reaching the end of the block + iter.currentPos = fm.BlockSize() + if iter.HasNext() { + t.Errorf("Expected HasNext to be false after reaching end of block, got true") + } +} + +func TestMultipleBlocks(t *testing.T) { + fm, tempDir := setupTestFileMgr(t) + defer func() { + fm.Close() + os.RemoveAll(tempDir) + }() + + filename := "test.db" + block1 := kfile.NewBlockId(filename, 1) + block2 := kfile.NewBlockId(filename, 0) + + // Write records to two blocks + record1 := []byte("record in block 1") + record2 := []byte("record in block 0") + rec3 := make([]byte, fm.BlockSize()) + rec4 := make([]byte, fm.BlockSize()) + copy(rec3, record1) + copy(rec4, record2) + page1 := kfile.NewPageFromBytes(rec3) + page2 := kfile.NewPageFromBytes(rec4) + page1.SetBytes(200, record1) + page1.SetInt(0, 200) // Set boundary for block 1 + page2.SetBytes(225, record2) + page2.SetInt(0, 225) // Set boundary for block 0 + + err := fm.Write(block1, page1) + if err != nil { + t.Fatalf("Failed to write block1: %v", err) + } + + err = fm.Write(block2, page2) + if err != nil { + t.Fatalf("Failed to write block2: %v", err) + } + + // Initialize LogIterator with the most recent block (block1) + iter := NewLogIterator(fm, block1) + + // Retrieve record from block 1 + if !iter.HasNext() { + t.Fatalf("Expected HasNext to be true for block1") + } + rec1, _ := iter.Next() + if string(rec1) != string(record1) { + t.Errorf("Expected record '%s', got '%s'", string(record1), string(rec1)) + } + + // Retrieve record from block 0 after transitioning + if !iter.HasNext() { + t.Fatalf("Expected HasNext to be true for block0") + } + rec2, _ := iter.Next() + if string(rec2) != string(record2) { + t.Errorf("Expected record '%s', got '%s'", string(record2), string(rec2)) + } +} diff --git a/utils/utility.go b/utils/utility.go new file mode 100644 index 0000000..ede0325 --- /dev/null +++ b/utils/utility.go @@ -0,0 +1,12 @@ +package utils + +import "unicode/utf8" + +func MaxLength(strlen int) int { + if strlen < 0 { + panic("String length cannot be negative") + } + // Estimate bytes required for UTF-8 encoding + bytesPerChar := utf8.RuneLen('a') // Assuming single-character equivalence + return 4 + (strlen * bytesPerChar) +}