1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
//! Phase OPT-F regression: in-place small→small realloc returns the same
//! pointer when the new size fits in the old class (new_class_idx <= old_class_idx).
//!
//! **Adapted from the task spec** — the original spec used 64→96→128 B examples
//! that happen to be *different* size classes in the actual `SIZE_CLASS_TABLE`
//! (class[3]=64, class[5]=112). Tests have been corrected to use sizes that
//! actually map to the same or smaller class:
//!
//! - class[3] block_size=64 covers sizes 49..=64
//! - class[2] block_size=48 covers sizes 33..=48
//! - class[1] block_size=32 covers sizes 17..=32
//!
//! The cross-class test grows 64→65 (class[3]→class[4]=80), which takes the
//! alloc+copy+dealloc path.
#![cfg(feature = "alloc-core")]
use core::alloc::Layout;
use sefer_alloc::AllocCore;
/// Realloc to the exact same size in the same class (no-op case).
#[test]
fn realloc_same_size_returns_same_ptr() {
let mut ac = AllocCore::new().expect("primordial");
let layout = Layout::from_size_align(64, 8).unwrap();
let ptr = ac.alloc(layout);
assert!(!ptr.is_null());
// Write a marker byte.
unsafe { ptr.write(0xAA) };
// 64 → 64: trivially same class[3] (block_size=64).
let new_ptr = ac.realloc(ptr, layout, 64);
assert!(!new_ptr.is_null());
assert_eq!(
ptr, new_ptr,
"realloc 64->64 must reuse the same block (trivially same class)"
);
assert_eq!(unsafe { new_ptr.read() }, 0xAA, "marker must survive");
ac.dealloc(new_ptr, Layout::from_size_align(64, 8).unwrap());
}
/// Realloc to a smaller size that stays in the same class (class[3]: 49..=64).
#[test]
fn realloc_shrink_within_same_class_returns_same_ptr() {
let mut ac = AllocCore::new().expect("primordial");
let old_layout = Layout::from_size_align(64, 8).unwrap();
let ptr = ac.alloc(old_layout);
assert!(!ptr.is_null());
unsafe { ptr.write(0xBB) };
// 64 → 56: still class[3] (block_size=64 covers 49..=64).
let new_ptr = ac.realloc(ptr, old_layout, 56);
assert!(!new_ptr.is_null());
assert_eq!(
ptr, new_ptr,
"realloc 64->56 must reuse the same block (still class[3])"
);
assert_eq!(unsafe { new_ptr.read() }, 0xBB, "marker must survive");
ac.dealloc(new_ptr, Layout::from_size_align(56, 8).unwrap());
}
/// Realloc that shrinks across classes: old class[3] (block_size=64),
/// new size=32 → class[1] (block_size=32). new_class_idx < old_class_idx,
/// so the block physically fits and must be reused in-place.
#[test]
fn realloc_shrink_cross_class_down_returns_same_ptr() {
let mut ac = AllocCore::new().expect("primordial");
let old_layout = Layout::from_size_align(64, 8).unwrap();
let ptr = ac.alloc(old_layout);
assert!(!ptr.is_null());
unsafe { ptr.write(0xCC) };
// 64 (class[3]) → 32 (class[1]): new_class_idx=1 < old_class_idx=3,
// so the new size fits inside the existing 64-byte block.
let new_ptr = ac.realloc(ptr, old_layout, 32);
assert!(!new_ptr.is_null());
assert_eq!(
ptr, new_ptr,
"realloc 64->32 must reuse the same block (shrink: class[1] <= class[3])"
);
assert_eq!(unsafe { new_ptr.read() }, 0xCC, "marker must survive");
ac.dealloc(new_ptr, Layout::from_size_align(32, 8).unwrap());
}
/// Realloc that grows into a LARGER class: 64 (class[3]) → 65 (class[4],
/// block_size=80). new_class_idx > old_class_idx — must allocate a new block
/// and copy. The test verifies data is preserved but does NOT assert ptr equality
/// (the ptr may change; what matters is the content is preserved and the old
/// pointer is freed).
#[test]
fn realloc_cross_class_up_moves_and_preserves_data() {
let mut ac = AllocCore::new().expect("primordial");
let old_layout = Layout::from_size_align(64, 8).unwrap();
let ptr = ac.alloc(old_layout);
assert!(!ptr.is_null());
// Write a pattern across all 64 bytes.
unsafe {
for i in 0..64usize {
ptr.add(i).write((i as u8).wrapping_add(0x20));
}
}
// 64 (class[3]) → 65 (class[4]=80): definitely a different, larger class.
let new_ptr = ac.realloc(ptr, old_layout, 65);
assert!(!new_ptr.is_null(), "realloc 64->65 must not return null");
// The first 64 bytes of data must be preserved (copy happened).
unsafe {
for i in 0..64usize {
assert_eq!(
new_ptr.add(i).read(),
(i as u8).wrapping_add(0x20),
"prefix byte {} lost during cross-class realloc 64->65",
i
);
}
}
ac.dealloc(new_ptr, Layout::from_size_align(65, 8).unwrap());
}
/// Sanity: realloc of a large allocation (size > SMALL_MAX) takes the slow
/// path. Data must be preserved.
#[test]
fn realloc_large_to_large_preserves_data() {
let mut ac = AllocCore::new().expect("primordial");
// Use a size larger than SMALL_MAX so both paths go through the large path.
let old_size = 128 * 1024; // 128 KiB — definitely large.
let old_layout = Layout::from_size_align(old_size, 16).unwrap();
let ptr = ac.alloc(old_layout);
assert!(!ptr.is_null());
unsafe {
for i in 0..64usize {
ptr.add(i).write((i as u8).wrapping_add(0x40));
}
}
let new_size = 256 * 1024;
let new_ptr = ac.realloc(ptr, old_layout, new_size);
assert!(!new_ptr.is_null());
unsafe {
for i in 0..64usize {
assert_eq!(
new_ptr.add(i).read(),
(i as u8).wrapping_add(0x40),
"prefix byte {} lost during large realloc",
i
);
}
}
ac.dealloc(new_ptr, Layout::from_size_align(new_size, 16).unwrap());
}