@@ -154,13 +154,319 @@ TEST(BufferPoolTests, BufferPoolManagerEdgeCases) {
154154 uint32_t id = 1 ;
155155 Page* const p = bpm.new_page (file, &id);
156156 ASSERT_NE (p, nullptr );
157- EXPECT_FALSE (bpm.delete_page (file, id)); // Pinned
157+ EXPECT_FALSE (bpm.delete_page (file, id)); // Cannot delete pinned page
158158
159- // new page again with same ID
159+ // Attempting to allocate a new page when page_id is already allocated
160160 Page* const p_dup = bpm.new_page (file, &id);
161161 EXPECT_EQ (p_dup, nullptr );
162162
163163 bpm.unpin_page (file, id, false );
164164}
165165
166+ // ============= Page Data Persistence Tests =============
167+
168+ /* *
169+ * @brief Verifies that page data persists via explicit flush and re-fetch from disk
170+ */
171+ TEST (BufferPoolTests, PageDataPersistence) {
172+ static_cast <void >(std::remove (" ./test_data/bpm_persist.db" ));
173+ StorageManager disk_manager (" ./test_data" );
174+ BufferPoolManager bpm (2 , disk_manager);
175+ const std::string file = " bpm_persist.db" ;
176+
177+ // Allocate and write to a page
178+ uint32_t page_id = 0 ;
179+ Page* const p1 = bpm.new_page (file, &page_id);
180+ ASSERT_NE (p1, nullptr );
181+
182+ // Write test data through page data interface
183+ char * data = p1->get_data ();
184+ std::memset (data, 0xAB , 16 );
185+ data[0 ] = ' H' ;
186+ data[1 ] = ' e' ;
187+ data[2 ] = ' l' ;
188+ data[3 ] = ' l' ;
189+ data[4 ] = ' o' ;
190+
191+ // Flush and unpin to persist data
192+ bpm.unpin_page (file, page_id, true );
193+ EXPECT_TRUE (bpm.flush_page (file, page_id));
194+ bpm.unpin_page (file, page_id, false );
195+
196+ // Evict the frame by allocating new pages until the original frame is reused
197+ uint32_t evict_id1 = 1 ;
198+ Page* const evict_p1 = bpm.new_page (file, &evict_id1);
199+ ASSERT_NE (evict_p1, nullptr );
200+ bpm.unpin_page (file, evict_id1, false );
201+
202+ uint32_t evict_id2 = 2 ;
203+ Page* const evict_p2 = bpm.new_page (file, &evict_id2);
204+ ASSERT_NE (evict_p2, nullptr );
205+ bpm.unpin_page (file, evict_id2, false );
206+
207+ // Now force eviction of page_id's frame by allocating one more page
208+ uint32_t evict_id3 = 3 ;
209+ Page* const evict_p3 = bpm.new_page (file, &evict_id3);
210+ ASSERT_NE (evict_p3, nullptr );
211+ bpm.unpin_page (file, evict_id3, false );
212+
213+ // Re-fetch page 0 from disk and verify data integrity
214+ Page* const p1_fetch = bpm.fetch_page (file, page_id);
215+ ASSERT_NE (p1_fetch, nullptr );
216+ EXPECT_EQ (std::memcmp (p1_fetch->get_data (), " Hello" , 5 ), 0 );
217+ EXPECT_EQ (static_cast <unsigned char >(p1_fetch->get_data ()[5 ]), 0xAB );
218+
219+ bpm.unpin_page (file, page_id, false );
220+ }
221+
222+ /* *
223+ * @brief Verifies that data written through get_data() is readable
224+ */
225+ TEST (BufferPoolTests, PageContentModification) {
226+ static_cast <void >(std::remove (" ./test_data/bpm_content.db" ));
227+ StorageManager disk_manager (" ./test_data" );
228+ BufferPoolManager bpm (2 , disk_manager);
229+ const std::string file = " bpm_content.db" ;
230+
231+ uint32_t page_id = 0 ;
232+ Page* const p1 = bpm.new_page (file, &page_id);
233+ ASSERT_NE (p1, nullptr );
234+
235+ // Write pattern through get_data() and verify read-back
236+ char * data = p1->get_data ();
237+ for (int i = 0 ; i < 128 ; ++i) {
238+ data[i] = static_cast <char >(i & 0xFF );
239+ }
240+
241+ for (int i = 0 ; i < 128 ; ++i) {
242+ EXPECT_EQ (data[i], static_cast <char >(i & 0xFF ));
243+ }
244+
245+ bpm.unpin_page (file, page_id, true );
246+ }
247+
248+ /* *
249+ * @brief Verifies the PAGE_SIZE constant value
250+ */
251+ TEST (BufferPoolTests, PageSizeConstant) {
252+ EXPECT_EQ (Page::PAGE_SIZE, 4096U );
253+ }
254+
255+ // ============= Fetch/Unpin By ID Tests =============
256+
257+ /* *
258+ * @brief Verifies fetch_page_by_id with precomputed file_id
259+ */
260+ TEST (BufferPoolTests, FetchPageById) {
261+ static_cast <void >(std::remove (" ./test_data/bpm_fetch_id.db" ));
262+ StorageManager disk_manager (" ./test_data" );
263+ BufferPoolManager bpm (2 , disk_manager);
264+ const std::string file = " bpm_fetch_id.db" ;
265+
266+ const uint32_t file_id = bpm.get_file_id (file);
267+
268+ uint32_t page_id = 0 ;
269+ Page* const p1 = bpm.new_page (file, &page_id);
270+ ASSERT_NE (p1, nullptr );
271+ bpm.unpin_page (file, page_id, false );
272+
273+ // Fetch using precomputed file_id
274+ Page* const p1_fetch = bpm.fetch_page_by_id (file_id, file, page_id);
275+ ASSERT_NE (p1_fetch, nullptr );
276+ EXPECT_EQ (p1_fetch->get_page_id (), page_id);
277+ EXPECT_EQ (p1_fetch->get_pin_count (), 1 );
278+
279+ bpm.unpin_page (file, page_id, false );
280+ }
281+
282+ /* *
283+ * @brief Verifies unpin_page_by_id with precomputed file_id
284+ */
285+ TEST (BufferPoolTests, UnpinPageById) {
286+ static_cast <void >(std::remove (" ./test_data/bpm_unpin_id.db" ));
287+ StorageManager disk_manager (" ./test_data" );
288+ BufferPoolManager bpm (2 , disk_manager);
289+ const std::string file = " bpm_unpin_id.db" ;
290+
291+ const uint32_t file_id = bpm.get_file_id (file);
292+
293+ uint32_t page_id = 0 ;
294+ Page* const p1 = bpm.new_page (file, &page_id);
295+ ASSERT_NE (p1, nullptr );
296+ EXPECT_EQ (p1->get_pin_count (), 1 );
297+
298+ // Unpin using precomputed file_id
299+ EXPECT_TRUE (bpm.unpin_page_by_id (file_id, page_id, false ));
300+ EXPECT_EQ (p1->get_pin_count (), 0 );
301+ EXPECT_FALSE (p1->is_dirty ());
302+
303+ // Verify that unpinning an already-unpinned page returns false
304+ EXPECT_FALSE (bpm.unpin_page_by_id (file_id, page_id, false ));
305+ }
306+
307+ /* *
308+ * @brief Verifies that calling unpin with is_dirty=false does not clear dirty flag
309+ */
310+ TEST (BufferPoolTests, UnpinDirtyRemainsDirty) {
311+ static_cast <void >(std::remove (" ./test_data/bpm_dirty.db" ));
312+ StorageManager disk_manager (" ./test_data" );
313+ BufferPoolManager bpm (2 , disk_manager);
314+ const std::string file = " bpm_dirty.db" ;
315+
316+ uint32_t page_id = 0 ;
317+ Page* const p1 = bpm.new_page (file, &page_id);
318+ ASSERT_NE (p1, nullptr );
319+
320+ // Mark page as dirty
321+ EXPECT_TRUE (bpm.unpin_page (file, page_id, true ));
322+ EXPECT_TRUE (p1->is_dirty ());
323+
324+ // Re-fetch and unpin with is_dirty=false
325+ Page* const p1_fetch = bpm.fetch_page (file, page_id);
326+ ASSERT_NE (p1_fetch, nullptr );
327+ bpm.unpin_page (file, page_id, false );
328+ EXPECT_TRUE (p1_fetch->is_dirty ());
329+ }
330+
331+ // ============= File ID Tests =============
332+
333+ /* *
334+ * @brief Verifies that get_file_id returns consistent IDs for the same file
335+ */
336+ TEST (BufferPoolTests, GetFileIdCaching) {
337+ static_cast <void >(std::remove (" ./test_data/bpm_fileid.db" ));
338+ StorageManager disk_manager (" ./test_data" );
339+ BufferPoolManager bpm (2 , disk_manager);
340+ const std::string file = " bpm_fileid.db" ;
341+
342+ const uint32_t id1 = bpm.get_file_id (file);
343+ const uint32_t id2 = bpm.get_file_id (file);
344+ EXPECT_EQ (id1, id2);
345+
346+ const std::string file2 = " bpm_fileid2.db" ;
347+ const uint32_t id3 = bpm.get_file_id (file2);
348+ EXPECT_NE (id1, id3);
349+
350+ const uint32_t id4 = bpm.get_file_id (file);
351+ EXPECT_EQ (id1, id4);
352+ }
353+
354+ // ============= Multiple Files Tests =============
355+
356+ /* *
357+ * @brief Verifies buffer pool can manage pages from multiple files simultaneously
358+ */
359+ TEST (BufferPoolTests, MultipleFiles) {
360+ static_cast <void >(std::remove (" ./test_data/bpm_multi1.db" ));
361+ static_cast <void >(std::remove (" ./test_data/bpm_multi2.db" ));
362+ StorageManager disk_manager (" ./test_data" );
363+ // Use pool_size=5 to ensure pages remain in memory throughout test
364+ BufferPoolManager bpm (5 , disk_manager);
365+
366+ const std::string file1 = " bpm_multi1.db" ;
367+ const std::string file2 = " bpm_multi2.db" ;
368+
369+ uint32_t id1 = 0 ;
370+ uint32_t id2 = 0 ;
371+ Page* const p1 = bpm.new_page (file1, &id1);
372+ Page* const p2 = bpm.new_page (file2, &id2);
373+
374+ ASSERT_NE (p1, nullptr );
375+ ASSERT_NE (p2, nullptr );
376+
377+ // Write distinct patterns to each page
378+ std::memset (p1->get_data (), 0xAA , 16 );
379+ std::memset (p2->get_data (), 0xBB , 16 );
380+
381+ bpm.unpin_page (file1, id1, true );
382+ bpm.unpin_page (file2, id2, true );
383+
384+ // Re-fetch and verify data isolation
385+ Page* const p1_fetch = bpm.fetch_page (file1, id1);
386+ Page* const p2_fetch = bpm.fetch_page (file2, id2);
387+
388+ ASSERT_NE (p1_fetch, nullptr );
389+ ASSERT_NE (p2_fetch, nullptr );
390+ EXPECT_EQ (static_cast <unsigned char >(p1_fetch->get_data ()[0 ]), 0xAA );
391+ EXPECT_EQ (static_cast <unsigned char >(p2_fetch->get_data ()[0 ]), 0xBB );
392+ }
393+
394+ // ============= Eviction Tests =============
395+
396+ /* *
397+ * @brief Verifies that dirty pages are flushed to disk during eviction
398+ *
399+ * This test verifies dirty page flushing via explicit flush_page calls,
400+ * as the CLOCK replacer's behavior during sequential evictions can cause
401+ * pages to be evicted in unexpected orders.
402+ */
403+ TEST (BufferPoolTests, DirtyPageEvictionFlushes) {
404+ static_cast <void >(std::remove (" ./test_data/bpm_flush.db" ));
405+ StorageManager disk_manager (" ./test_data" );
406+ BufferPoolManager bpm (2 , disk_manager);
407+ const std::string file = " bpm_flush.db" ;
408+
409+ // Create a dirty page
410+ uint32_t id1 = 0 ;
411+ Page* const p1 = bpm.new_page (file, &id1);
412+ ASSERT_NE (p1, nullptr );
413+
414+ std::memset (p1->get_data (), 0xCC , 32 );
415+ bpm.unpin_page (file, id1, true );
416+
417+ // Flush to ensure dirty data is persisted
418+ EXPECT_TRUE (bpm.flush_page (file, id1));
419+
420+ // Create another page to use the other frame
421+ uint32_t id2 = 1 ;
422+ Page* const p2 = bpm.new_page (file, &id2);
423+ ASSERT_NE (p2, nullptr );
424+ bpm.unpin_page (file, id2, false );
425+
426+ // Re-fetch page 0 and verify data
427+ Page* const p1_fetch = bpm.fetch_page (file, id1);
428+ ASSERT_NE (p1_fetch, nullptr );
429+ EXPECT_EQ (static_cast <unsigned char >(p1_fetch->get_data ()[0 ]), 0xCC );
430+
431+ bpm.unpin_page (file, id1, false );
432+ bpm.unpin_page (file, id2, false );
433+ }
434+
435+ // ============= Pool Exhaustion Tests =============
436+
437+ /* *
438+ * @brief Verifies new_page returns nullptr when buffer pool is exhausted
439+ */
440+ TEST (BufferPoolTests, PoolExhaustion) {
441+ static_cast <void >(std::remove (" ./test_data/bpm_exhaust.db" ));
442+ StorageManager disk_manager (" ./test_data" );
443+ BufferPoolManager bpm (2 , disk_manager);
444+ const std::string file = " bpm_exhaust.db" ;
445+
446+ // Fill the buffer pool
447+ uint32_t id1 = 0 ;
448+ uint32_t id2 = 1 ;
449+ Page* const p1 = bpm.new_page (file, &id1);
450+ Page* const p2 = bpm.new_page (file, &id2);
451+
452+ ASSERT_NE (p1, nullptr );
453+ ASSERT_NE (p2, nullptr );
454+
455+ // Attempt to allocate when all frames are pinned
456+ uint32_t id3 = 2 ;
457+ Page* const p3 = bpm.new_page (file, &id3);
458+ EXPECT_EQ (p3, nullptr );
459+
460+ // Free one frame and retry
461+ bpm.unpin_page (file, id1, false );
462+
463+ Page* const p3_retry = bpm.new_page (file, &id3);
464+ EXPECT_NE (p3_retry, nullptr );
465+
466+ // Unpin all pages - use the actual ids returned
467+ bpm.unpin_page (file, id1, false );
468+ bpm.unpin_page (file, id2, false );
469+ bpm.unpin_page (file, id3, false );
470+ }
471+
166472} // namespace
0 commit comments