prometheus-client-mmap 0.21.0-x86_64-linux → 0.22.0-x86_64-linux
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/ext/fast_mmaped_file_rs/src/mmap/inner.rs +708 -0
- data/ext/fast_mmaped_file_rs/src/mmap.rs +587 -25
- data/lib/2.7/fast_mmaped_file_rs.so +0 -0
- data/lib/3.0/fast_mmaped_file_rs.so +0 -0
- data/lib/3.1/fast_mmaped_file_rs.so +0 -0
- data/lib/3.2/fast_mmaped_file_rs.so +0 -0
- data/lib/prometheus/client/version.rb +1 -1
- metadata +7 -2
@@ -0,0 +1,708 @@
|
|
1
|
+
use libc::off_t;
|
2
|
+
use memmap2::{MmapMut, MmapOptions};
|
3
|
+
use nix::libc::c_long;
|
4
|
+
use std::fs::File;
|
5
|
+
use std::mem::size_of;
|
6
|
+
use std::ops::Range;
|
7
|
+
use std::os::unix::prelude::{AsRawFd, RawFd};
|
8
|
+
use std::path::PathBuf;
|
9
|
+
|
10
|
+
use crate::error::{MmapError, RubyError};
|
11
|
+
use crate::raw_entry::RawEntry;
|
12
|
+
use crate::util::CheckedOps;
|
13
|
+
use crate::util::{self, errno, read_f64, read_u32};
|
14
|
+
use crate::Result;
|
15
|
+
use crate::HEADER_SIZE;
|
16
|
+
|
17
|
+
/// A mmapped file and its metadata. Ruby never directly interfaces
|
18
|
+
/// with this struct.
|
19
|
+
#[derive(Debug)]
|
20
|
+
pub(super) struct InnerMmap {
|
21
|
+
/// The handle of the file being mmapped. When resizing the
|
22
|
+
/// file we must drop the `InnerMmap` while keeping this open,
|
23
|
+
/// truncate/extend the file, and establish a new `InnerMmap` to
|
24
|
+
/// re-map it.
|
25
|
+
file: File,
|
26
|
+
/// The path of the file.
|
27
|
+
path: PathBuf,
|
28
|
+
/// The mmap itself. When initializing a new entry the length of
|
29
|
+
/// the mmap is used for bounds checking.
|
30
|
+
map: MmapMut,
|
31
|
+
/// The length of data written to the file, used to validate
|
32
|
+
/// whether a `load/save_value` call is in bounds and the length
|
33
|
+
/// we truncate the file to when unmapping.
|
34
|
+
///
|
35
|
+
/// Equivalent to `i_mm->t->real` in the C implementation.
|
36
|
+
len: usize,
|
37
|
+
}
|
38
|
+
|
39
|
+
impl InnerMmap {
|
40
|
+
/// Constructs a new `InnerMmap`, mmapping `path`.
|
41
|
+
/// Use when mmapping a file for the first time. When re-mapping a file
|
42
|
+
/// after expanding it the `reestablish` function should be used.
|
43
|
+
pub fn new(path: PathBuf, file: File) -> Result<Self> {
|
44
|
+
let stat = file.metadata().map_err(|e| {
|
45
|
+
MmapError::legacy(
|
46
|
+
format!("Can't stat {}: {e}", path.display()),
|
47
|
+
RubyError::Arg,
|
48
|
+
)
|
49
|
+
})?;
|
50
|
+
|
51
|
+
let file_size = util::cast_chk::<_, usize>(stat.len(), "file length")?;
|
52
|
+
|
53
|
+
// We need to ensure the underlying file descriptor is at least a page size.
|
54
|
+
// Otherwise, we could get a SIGBUS error if mmap() attempts to read or write
|
55
|
+
// past the file.
|
56
|
+
let reserve_size = Self::next_page_boundary(file_size)?;
|
57
|
+
|
58
|
+
// Cast: no-op.
|
59
|
+
Self::reserve_mmap_file_bytes(file.as_raw_fd(), reserve_size as off_t).map_err(|e| {
|
60
|
+
MmapError::legacy(
|
61
|
+
format!(
|
62
|
+
"Can't reserve {reserve_size} bytes for memory-mapped file in {}: {e}",
|
63
|
+
path.display()
|
64
|
+
),
|
65
|
+
RubyError::Io,
|
66
|
+
)
|
67
|
+
})?;
|
68
|
+
|
69
|
+
// Ensure we always have space for the header.
|
70
|
+
let map_len = file_size.max(HEADER_SIZE);
|
71
|
+
|
72
|
+
// SAFETY: There is the possibility of UB if the file is modified outside of
|
73
|
+
// this program.
|
74
|
+
let map = unsafe { MmapOptions::new().len(map_len).map_mut(&file) }.map_err(|e| {
|
75
|
+
MmapError::legacy(format!("mmap failed ({}): {e}", errno()), RubyError::Arg)
|
76
|
+
})?;
|
77
|
+
|
78
|
+
let len = file_size;
|
79
|
+
|
80
|
+
Ok(Self {
|
81
|
+
file,
|
82
|
+
path,
|
83
|
+
map,
|
84
|
+
len,
|
85
|
+
})
|
86
|
+
}
|
87
|
+
|
88
|
+
/// Re-mmap a file that was previously mapped.
|
89
|
+
pub fn reestablish(path: PathBuf, file: File, map_len: usize) -> Result<Self> {
|
90
|
+
// SAFETY: There is the possibility of UB if the file is modified outside of
|
91
|
+
// this program.
|
92
|
+
let map = unsafe { MmapOptions::new().len(map_len).map_mut(&file) }.map_err(|e| {
|
93
|
+
MmapError::legacy(format!("mmap failed ({}): {e}", errno()), RubyError::Arg)
|
94
|
+
})?;
|
95
|
+
|
96
|
+
// TODO should we keep this as the old len? We'd want to be able to truncate
|
97
|
+
// to the old length at this point if closing the file. Matching C implementation
|
98
|
+
// for now.
|
99
|
+
let len = map_len;
|
100
|
+
|
101
|
+
Ok(Self {
|
102
|
+
file,
|
103
|
+
path,
|
104
|
+
map,
|
105
|
+
len,
|
106
|
+
})
|
107
|
+
}
|
108
|
+
|
109
|
+
/// Add a new metrics entry to the end of the mmap. This will fail if the mmap is at
|
110
|
+
/// capacity. Callers must expand the file first.
|
111
|
+
///
|
112
|
+
/// SAFETY: Must not call any Ruby code for the lifetime of `key`, otherwise we risk
|
113
|
+
/// Ruby mutating the underlying `RString`.
|
114
|
+
pub unsafe fn initialize_entry(&mut self, key: &[u8], value: f64) -> Result<usize> {
|
115
|
+
// CAST: no-op on 32-bit, widening on 64-bit.
|
116
|
+
let current_used = self.load_used()? as usize;
|
117
|
+
let entry_length = RawEntry::calc_total_len(key.len())?;
|
118
|
+
|
119
|
+
let new_used = current_used.add_chk(entry_length)?;
|
120
|
+
|
121
|
+
// Increasing capacity requires expanding the file and re-mmapping it, we can't
|
122
|
+
// perform this from `InnerMmap`.
|
123
|
+
if self.capacity() < new_used {
|
124
|
+
return Err(MmapError::Other(format!(
|
125
|
+
"mmap capacity {} less than {}",
|
126
|
+
self.capacity(),
|
127
|
+
new_used
|
128
|
+
)));
|
129
|
+
}
|
130
|
+
|
131
|
+
let bytes = self.map.as_mut();
|
132
|
+
let value_offset = RawEntry::save(&mut bytes[current_used..new_used], key, value)?;
|
133
|
+
|
134
|
+
// Won't overflow as value_offset is less than new_used.
|
135
|
+
let position = current_used + value_offset;
|
136
|
+
let new_used32 = util::cast_chk::<_, u32>(new_used, "used")?;
|
137
|
+
|
138
|
+
self.save_used(new_used32)?;
|
139
|
+
Ok(position)
|
140
|
+
}
|
141
|
+
|
142
|
+
/// Save a metrics value to an existing entry in the mmap.
|
143
|
+
pub fn save_value(&mut self, offset: usize, value: f64) -> Result<()> {
|
144
|
+
if self.len.add_chk(size_of::<f64>())? <= offset {
|
145
|
+
return Err(MmapError::out_of_bounds(
|
146
|
+
offset + size_of::<f64>(),
|
147
|
+
self.len,
|
148
|
+
));
|
149
|
+
}
|
150
|
+
|
151
|
+
if offset < HEADER_SIZE {
|
152
|
+
return Err(MmapError::Other(format!(
|
153
|
+
"writing to offset {offset} would overwrite file header"
|
154
|
+
)));
|
155
|
+
}
|
156
|
+
|
157
|
+
let value_bytes = value.to_ne_bytes();
|
158
|
+
let value_range = self.item_range(offset, value_bytes.len())?;
|
159
|
+
|
160
|
+
let bytes = self.map.as_mut();
|
161
|
+
bytes[value_range].copy_from_slice(&value_bytes);
|
162
|
+
|
163
|
+
Ok(())
|
164
|
+
}
|
165
|
+
|
166
|
+
/// Load a metrics value from an entry in the mmap.
|
167
|
+
pub fn load_value(&self, offset: usize) -> Result<f64> {
|
168
|
+
if self.len.add_chk(size_of::<f64>())? <= offset {
|
169
|
+
return Err(MmapError::out_of_bounds(
|
170
|
+
offset + size_of::<f64>(),
|
171
|
+
self.len,
|
172
|
+
));
|
173
|
+
}
|
174
|
+
read_f64(self.map.as_ref(), offset)
|
175
|
+
}
|
176
|
+
|
177
|
+
/// The length of data written to the file.
|
178
|
+
/// With a new file this is only set when Ruby calls `slice` on
|
179
|
+
/// `FastMmapedFileRs`, so even if data has been written to the
|
180
|
+
/// mmap attempts to read will fail until a String is created.
|
181
|
+
/// When an existing file is read we set this value immediately.
|
182
|
+
///
|
183
|
+
/// Equivalent to `i_mm->t->real` in the C implementation.
|
184
|
+
#[inline]
|
185
|
+
pub fn len(&self) -> usize {
|
186
|
+
self.len
|
187
|
+
}
|
188
|
+
|
189
|
+
/// The total length in bytes of the mmapped file.
|
190
|
+
///
|
191
|
+
/// Equivalent to `i_mm->t->len` in the C implementation.
|
192
|
+
#[inline]
|
193
|
+
pub fn capacity(&self) -> usize {
|
194
|
+
self.map.len()
|
195
|
+
}
|
196
|
+
|
197
|
+
/// Update the length of the mmap considered to be written.
|
198
|
+
pub fn set_len(&mut self, len: usize) {
|
199
|
+
self.len = len;
|
200
|
+
}
|
201
|
+
|
202
|
+
/// Returns a raw pointer to the mmap.
|
203
|
+
pub fn as_ptr(&self) -> *const u8 {
|
204
|
+
self.map.as_ptr()
|
205
|
+
}
|
206
|
+
|
207
|
+
/// Perform an msync(2) on the mmap, flushing all changes written
|
208
|
+
/// to disk. The sync may optionally be performed asynchronously.
|
209
|
+
pub fn flush(&mut self, f_async: bool) -> Result<()> {
|
210
|
+
if f_async {
|
211
|
+
self.map
|
212
|
+
.flush_async()
|
213
|
+
.map_err(|_| MmapError::legacy(format!("msync({})", errno()), RubyError::Arg))
|
214
|
+
} else {
|
215
|
+
self.map
|
216
|
+
.flush()
|
217
|
+
.map_err(|_| MmapError::legacy(format!("msync({})", errno()), RubyError::Arg))
|
218
|
+
}
|
219
|
+
}
|
220
|
+
|
221
|
+
/// Truncate the mmapped file to the end of the metrics data.
|
222
|
+
pub fn truncate_file(&mut self) -> Result<()> {
|
223
|
+
// CAST: no-op on 64-bit, widening on 32-bit.
|
224
|
+
let trunc_len = self.len as u64;
|
225
|
+
|
226
|
+
self.file
|
227
|
+
.set_len(trunc_len)
|
228
|
+
.map_err(|e| MmapError::legacy(format!("truncate: {e}"), RubyError::Type))
|
229
|
+
}
|
230
|
+
|
231
|
+
/// Load the `used` header containing the size of the metrics data written.
|
232
|
+
pub fn load_used(&self) -> Result<u32> {
|
233
|
+
match read_u32(self.map.as_ref(), 0) {
|
234
|
+
// CAST: we know HEADER_SIZE fits in a u32.
|
235
|
+
Ok(0) => Ok(HEADER_SIZE as u32),
|
236
|
+
u => u,
|
237
|
+
}
|
238
|
+
}
|
239
|
+
|
240
|
+
/// Update the `used` header to the value provided.
|
241
|
+
/// value provided.
|
242
|
+
pub fn save_used(&mut self, used: u32) -> Result<()> {
|
243
|
+
let bytes = self.map.as_mut();
|
244
|
+
bytes[..size_of::<u32>()].copy_from_slice(&used.to_ne_bytes());
|
245
|
+
|
246
|
+
Ok(())
|
247
|
+
}
|
248
|
+
|
249
|
+
/// Drop self, which performs an munmap(2) on the mmap,
|
250
|
+
/// returning the open `File` and `PathBuf` so the
|
251
|
+
/// caller can expand the file and re-mmap it.
|
252
|
+
pub fn munmap(self) -> (File, PathBuf) {
|
253
|
+
(self.file, self.path)
|
254
|
+
}
|
255
|
+
|
256
|
+
// From https://stackoverflow.com/a/22820221: The difference with
|
257
|
+
// ftruncate(2) is that (on file systems supporting it, e.g. Ext4)
|
258
|
+
// disk space is indeed reserved by posix_fallocate but ftruncate
|
259
|
+
// extends the file by adding holes (and without reserving disk
|
260
|
+
// space).
|
261
|
+
#[cfg(target_os = "linux")]
|
262
|
+
fn reserve_mmap_file_bytes(fd: RawFd, len: off_t) -> nix::Result<()> {
|
263
|
+
nix::fcntl::posix_fallocate(fd, 0, len)
|
264
|
+
}
|
265
|
+
|
266
|
+
// We simplify the reference implementation since we generally
|
267
|
+
// don't need to reserve more than a page size.
|
268
|
+
#[cfg(not(target_os = "linux"))]
|
269
|
+
fn reserve_mmap_file_bytes(fd: RawFd, len: off_t) -> nix::Result<()> {
|
270
|
+
nix::unistd::ftruncate(fd, len)
|
271
|
+
}
|
272
|
+
|
273
|
+
fn item_range(&self, start: usize, len: usize) -> Result<Range<usize>> {
|
274
|
+
let offset_end = start.add_chk(len)?;
|
275
|
+
|
276
|
+
if offset_end >= self.capacity() {
|
277
|
+
return Err(MmapError::out_of_bounds(offset_end, self.capacity()));
|
278
|
+
}
|
279
|
+
|
280
|
+
Ok(start..offset_end)
|
281
|
+
}
|
282
|
+
|
283
|
+
fn next_page_boundary(len: usize) -> Result<c_long> {
|
284
|
+
use nix::unistd::{self, SysconfVar};
|
285
|
+
|
286
|
+
let len = c_long::try_from(len)
|
287
|
+
.map_err(|_| MmapError::failed_cast::<_, c_long>(len, "file len"))?;
|
288
|
+
|
289
|
+
let mut page_size = match unistd::sysconf(SysconfVar::PAGE_SIZE) {
|
290
|
+
Ok(Some(p)) if p > 0 => p,
|
291
|
+
Ok(Some(p)) => {
|
292
|
+
return Err(MmapError::legacy(
|
293
|
+
format!("Invalid page size {p}"),
|
294
|
+
RubyError::Io,
|
295
|
+
))
|
296
|
+
}
|
297
|
+
Ok(None) => {
|
298
|
+
return Err(MmapError::legacy(
|
299
|
+
"No system page size found",
|
300
|
+
RubyError::Io,
|
301
|
+
))
|
302
|
+
}
|
303
|
+
Err(_) => {
|
304
|
+
return Err(MmapError::legacy(
|
305
|
+
"Failed to get system page size: {e}",
|
306
|
+
RubyError::Io,
|
307
|
+
))
|
308
|
+
}
|
309
|
+
};
|
310
|
+
|
311
|
+
while page_size < len {
|
312
|
+
page_size = page_size.mul_chk(2)?;
|
313
|
+
}
|
314
|
+
|
315
|
+
Ok(page_size)
|
316
|
+
}
|
317
|
+
}
|
318
|
+
|
319
|
+
#[cfg(test)]
|
320
|
+
mod test {
|
321
|
+
use nix::unistd::{self, SysconfVar};
|
322
|
+
|
323
|
+
use super::*;
|
324
|
+
use crate::testhelper::{self, TestEntry, TestFile};
|
325
|
+
use crate::HEADER_SIZE;
|
326
|
+
|
327
|
+
#[test]
|
328
|
+
fn test_new() {
|
329
|
+
struct TestCase {
|
330
|
+
name: &'static str,
|
331
|
+
existing: bool,
|
332
|
+
expected_len: usize,
|
333
|
+
}
|
334
|
+
|
335
|
+
let page_size = unistd::sysconf(SysconfVar::PAGE_SIZE).unwrap().unwrap();
|
336
|
+
|
337
|
+
let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
|
338
|
+
let value = 1.0;
|
339
|
+
let entry_len = TestEntry::new(json, value).as_bytes().len();
|
340
|
+
|
341
|
+
let tc = vec![
|
342
|
+
TestCase {
|
343
|
+
name: "empty file",
|
344
|
+
existing: false,
|
345
|
+
expected_len: 0,
|
346
|
+
},
|
347
|
+
TestCase {
|
348
|
+
name: "existing file",
|
349
|
+
existing: true,
|
350
|
+
expected_len: HEADER_SIZE + entry_len,
|
351
|
+
},
|
352
|
+
];
|
353
|
+
|
354
|
+
for case in tc {
|
355
|
+
let name = case.name;
|
356
|
+
|
357
|
+
let data = match case.existing {
|
358
|
+
true => testhelper::entries_to_db(&[json], &[1.0], None),
|
359
|
+
false => Vec::new(),
|
360
|
+
};
|
361
|
+
|
362
|
+
let TestFile {
|
363
|
+
file: original_file,
|
364
|
+
path,
|
365
|
+
dir: _dir,
|
366
|
+
} = TestFile::new(&data);
|
367
|
+
|
368
|
+
let original_stat = original_file.metadata().unwrap();
|
369
|
+
|
370
|
+
let inner = InnerMmap::new(path.clone(), original_file).unwrap();
|
371
|
+
|
372
|
+
let updated_file = File::open(&path).unwrap();
|
373
|
+
let updated_stat = updated_file.metadata().unwrap();
|
374
|
+
|
375
|
+
assert!(
|
376
|
+
updated_stat.len() > original_stat.len(),
|
377
|
+
"test case: {name} - file has been extended"
|
378
|
+
);
|
379
|
+
|
380
|
+
assert_eq!(
|
381
|
+
updated_stat.len(),
|
382
|
+
page_size as u64,
|
383
|
+
"test case: {name} - file extended to page size"
|
384
|
+
);
|
385
|
+
|
386
|
+
assert_eq!(
|
387
|
+
inner.capacity() as u64,
|
388
|
+
original_stat.len().max(HEADER_SIZE as u64),
|
389
|
+
"test case: {name} - mmap capacity matches original file len, unless smaller than HEADER_SIZE"
|
390
|
+
);
|
391
|
+
|
392
|
+
assert_eq!(
|
393
|
+
case.expected_len,
|
394
|
+
inner.len(),
|
395
|
+
"test case: {name} - len set"
|
396
|
+
);
|
397
|
+
}
|
398
|
+
}
|
399
|
+
|
400
|
+
#[test]
|
401
|
+
fn test_reestablish() {
|
402
|
+
struct TestCase {
|
403
|
+
name: &'static str,
|
404
|
+
target_len: usize,
|
405
|
+
expected_len: usize,
|
406
|
+
}
|
407
|
+
|
408
|
+
let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
|
409
|
+
|
410
|
+
let tc = vec![TestCase {
|
411
|
+
name: "ok",
|
412
|
+
target_len: 4096,
|
413
|
+
expected_len: 4096,
|
414
|
+
}];
|
415
|
+
|
416
|
+
for case in tc {
|
417
|
+
let name = case.name;
|
418
|
+
|
419
|
+
let data = testhelper::entries_to_db(&[json], &[1.0], None);
|
420
|
+
|
421
|
+
let TestFile {
|
422
|
+
file: original_file,
|
423
|
+
path,
|
424
|
+
dir: _dir,
|
425
|
+
} = TestFile::new(&data);
|
426
|
+
|
427
|
+
let inner =
|
428
|
+
InnerMmap::reestablish(path.clone(), original_file, case.target_len).unwrap();
|
429
|
+
|
430
|
+
assert_eq!(
|
431
|
+
case.target_len,
|
432
|
+
inner.capacity(),
|
433
|
+
"test case: {name} - mmap capacity set to target len",
|
434
|
+
);
|
435
|
+
|
436
|
+
assert_eq!(
|
437
|
+
case.expected_len,
|
438
|
+
inner.len(),
|
439
|
+
"test case: {name} - len set"
|
440
|
+
);
|
441
|
+
}
|
442
|
+
}
|
443
|
+
|
444
|
+
#[test]
|
445
|
+
fn test_initialize_entry() {
|
446
|
+
struct TestCase {
|
447
|
+
name: &'static str,
|
448
|
+
empty: bool,
|
449
|
+
used: Option<u32>,
|
450
|
+
expected_used: Option<u32>,
|
451
|
+
expected_value_offset: Option<usize>,
|
452
|
+
expected_err: Option<MmapError>,
|
453
|
+
}
|
454
|
+
|
455
|
+
let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
|
456
|
+
let value = 1.0;
|
457
|
+
let entry_len = TestEntry::new(json, value).as_bytes().len();
|
458
|
+
|
459
|
+
let tc = vec![
|
460
|
+
TestCase {
|
461
|
+
name: "empty file, not expanded by outer mmap",
|
462
|
+
empty: true,
|
463
|
+
used: None,
|
464
|
+
expected_used: None,
|
465
|
+
expected_value_offset: None,
|
466
|
+
expected_err: Some(MmapError::Other(format!(
|
467
|
+
"mmap capacity {HEADER_SIZE} less than {}",
|
468
|
+
entry_len + HEADER_SIZE,
|
469
|
+
))),
|
470
|
+
},
|
471
|
+
TestCase {
|
472
|
+
name: "data in file",
|
473
|
+
empty: false,
|
474
|
+
used: None,
|
475
|
+
expected_used: Some(HEADER_SIZE as u32 + (entry_len * 2) as u32),
|
476
|
+
expected_value_offset: Some(176),
|
477
|
+
expected_err: None,
|
478
|
+
},
|
479
|
+
TestCase {
|
480
|
+
name: "data in file, invalid used larger than file",
|
481
|
+
empty: false,
|
482
|
+
used: Some(10_000),
|
483
|
+
expected_used: None,
|
484
|
+
expected_value_offset: None,
|
485
|
+
expected_err: Some(MmapError::Other(format!(
|
486
|
+
"mmap capacity 4096 less than {}",
|
487
|
+
10_000 + entry_len
|
488
|
+
))),
|
489
|
+
},
|
490
|
+
];
|
491
|
+
|
492
|
+
for case in tc {
|
493
|
+
let name = case.name;
|
494
|
+
|
495
|
+
let data = match case.empty {
|
496
|
+
true => Vec::new(),
|
497
|
+
false => testhelper::entries_to_db(&[json], &[1.0], case.used),
|
498
|
+
};
|
499
|
+
|
500
|
+
let TestFile {
|
501
|
+
file,
|
502
|
+
path,
|
503
|
+
dir: _dir,
|
504
|
+
} = TestFile::new(&data);
|
505
|
+
|
506
|
+
if !case.empty {
|
507
|
+
// Ensure the file is large enough to have additional entries added.
|
508
|
+
// Normally the outer mmap handles this.
|
509
|
+
file.set_len(4096).unwrap();
|
510
|
+
}
|
511
|
+
let mut inner = InnerMmap::new(path, file).unwrap();
|
512
|
+
|
513
|
+
let result = unsafe { inner.initialize_entry(json.as_bytes(), value) };
|
514
|
+
|
515
|
+
if let Some(expected_used) = case.expected_used {
|
516
|
+
assert_eq!(
|
517
|
+
expected_used,
|
518
|
+
inner.load_used().unwrap(),
|
519
|
+
"test case: {name} - used"
|
520
|
+
);
|
521
|
+
}
|
522
|
+
|
523
|
+
if let Some(expected_value_offset) = case.expected_value_offset {
|
524
|
+
assert_eq!(
|
525
|
+
expected_value_offset,
|
526
|
+
*result.as_ref().unwrap(),
|
527
|
+
"test case: {name} - value_offset"
|
528
|
+
);
|
529
|
+
}
|
530
|
+
|
531
|
+
if let Some(expected_err) = case.expected_err {
|
532
|
+
assert_eq!(
|
533
|
+
expected_err,
|
534
|
+
result.unwrap_err(),
|
535
|
+
"test case: {name} - error"
|
536
|
+
);
|
537
|
+
}
|
538
|
+
}
|
539
|
+
}
|
540
|
+
|
541
|
+
#[test]
|
542
|
+
fn test_save_value() {
|
543
|
+
let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
|
544
|
+
let value = 1.0;
|
545
|
+
let upper_bound = TestEntry::new(json, value).as_bytes().len() + HEADER_SIZE;
|
546
|
+
let value_offset = upper_bound - size_of::<f64>();
|
547
|
+
|
548
|
+
struct TestCase {
|
549
|
+
name: &'static str,
|
550
|
+
empty: bool,
|
551
|
+
len: Option<usize>,
|
552
|
+
offset: usize,
|
553
|
+
expected_err: Option<MmapError>,
|
554
|
+
}
|
555
|
+
|
556
|
+
let tc = vec![
|
557
|
+
TestCase {
|
558
|
+
name: "existing file, in bounds",
|
559
|
+
empty: false,
|
560
|
+
len: None,
|
561
|
+
offset: upper_bound - size_of::<f64>() - 1,
|
562
|
+
expected_err: None,
|
563
|
+
},
|
564
|
+
TestCase {
|
565
|
+
name: "existing file, out of bounds",
|
566
|
+
empty: false,
|
567
|
+
len: Some(100),
|
568
|
+
offset: upper_bound * 2,
|
569
|
+
expected_err: Some(MmapError::out_of_bounds(
|
570
|
+
upper_bound * 2 + size_of::<f64>(),
|
571
|
+
100,
|
572
|
+
)),
|
573
|
+
},
|
574
|
+
TestCase {
|
575
|
+
name: "existing file, off by one",
|
576
|
+
empty: false,
|
577
|
+
len: None,
|
578
|
+
offset: value_offset + 1,
|
579
|
+
expected_err: Some(MmapError::out_of_bounds(
|
580
|
+
value_offset + 1 + size_of::<f64>(),
|
581
|
+
upper_bound,
|
582
|
+
)),
|
583
|
+
},
|
584
|
+
TestCase {
|
585
|
+
name: "empty file cannot be saved to",
|
586
|
+
empty: true,
|
587
|
+
len: None,
|
588
|
+
offset: 8,
|
589
|
+
expected_err: Some(MmapError::out_of_bounds(8 + size_of::<f64>(), 0)),
|
590
|
+
},
|
591
|
+
TestCase {
|
592
|
+
name: "overwrite header",
|
593
|
+
empty: false,
|
594
|
+
len: None,
|
595
|
+
offset: 7,
|
596
|
+
expected_err: Some(MmapError::Other(
|
597
|
+
"writing to offset 7 would overwrite file header".to_string(),
|
598
|
+
)),
|
599
|
+
},
|
600
|
+
];
|
601
|
+
|
602
|
+
for case in tc {
|
603
|
+
let name = case.name;
|
604
|
+
|
605
|
+
let mut data = match case.empty {
|
606
|
+
true => Vec::new(),
|
607
|
+
false => testhelper::entries_to_db(&[json], &[1.0], None),
|
608
|
+
};
|
609
|
+
|
610
|
+
if let Some(len) = case.len {
|
611
|
+
// Pad input to desired length.
|
612
|
+
data.append(&mut vec![0xff; len - upper_bound]);
|
613
|
+
}
|
614
|
+
|
615
|
+
let TestFile {
|
616
|
+
file,
|
617
|
+
path,
|
618
|
+
dir: _dir,
|
619
|
+
} = TestFile::new(&data);
|
620
|
+
|
621
|
+
let mut inner = InnerMmap::new(path, file).unwrap();
|
622
|
+
|
623
|
+
let result = inner.save_value(case.offset, value);
|
624
|
+
|
625
|
+
if let Some(expected_err) = case.expected_err {
|
626
|
+
assert_eq!(
|
627
|
+
expected_err,
|
628
|
+
result.unwrap_err(),
|
629
|
+
"test case: {name} - expected err"
|
630
|
+
);
|
631
|
+
} else {
|
632
|
+
assert!(result.is_ok(), "test case: {name} - success");
|
633
|
+
|
634
|
+
assert_eq!(
|
635
|
+
value,
|
636
|
+
util::read_f64(&inner.map, case.offset).unwrap(),
|
637
|
+
"test case: {name} - value saved"
|
638
|
+
);
|
639
|
+
}
|
640
|
+
}
|
641
|
+
}
|
642
|
+
|
643
|
+
#[test]
|
644
|
+
fn test_load_value() {
|
645
|
+
let json = r#"["first_family","first_name",["label_a","label_b"],["value_a","value_b"]]"#;
|
646
|
+
let value = 1.0;
|
647
|
+
let total_len = TestEntry::new(json, value).as_bytes().len() + HEADER_SIZE;
|
648
|
+
let value_offset = total_len - size_of::<f64>();
|
649
|
+
|
650
|
+
struct TestCase {
|
651
|
+
name: &'static str,
|
652
|
+
offset: usize,
|
653
|
+
expected_err: Option<MmapError>,
|
654
|
+
}
|
655
|
+
|
656
|
+
let tc = vec![
|
657
|
+
TestCase {
|
658
|
+
name: "in bounds",
|
659
|
+
offset: value_offset,
|
660
|
+
expected_err: None,
|
661
|
+
},
|
662
|
+
TestCase {
|
663
|
+
name: "out of bounds",
|
664
|
+
offset: value_offset * 2,
|
665
|
+
expected_err: Some(MmapError::out_of_bounds(
|
666
|
+
value_offset * 2 + size_of::<f64>(),
|
667
|
+
total_len,
|
668
|
+
)),
|
669
|
+
},
|
670
|
+
TestCase {
|
671
|
+
name: "off by one",
|
672
|
+
offset: value_offset + 1,
|
673
|
+
expected_err: Some(MmapError::out_of_bounds(
|
674
|
+
value_offset + 1 + size_of::<f64>(),
|
675
|
+
total_len,
|
676
|
+
)),
|
677
|
+
},
|
678
|
+
];
|
679
|
+
|
680
|
+
for case in tc {
|
681
|
+
let name = case.name;
|
682
|
+
|
683
|
+
let data = testhelper::entries_to_db(&[json], &[1.0], None);
|
684
|
+
|
685
|
+
let TestFile {
|
686
|
+
file,
|
687
|
+
path,
|
688
|
+
dir: _dir,
|
689
|
+
} = TestFile::new(&data);
|
690
|
+
|
691
|
+
let inner = InnerMmap::new(path, file).unwrap();
|
692
|
+
|
693
|
+
let result = inner.load_value(case.offset);
|
694
|
+
|
695
|
+
if let Some(expected_err) = case.expected_err {
|
696
|
+
assert_eq!(
|
697
|
+
expected_err,
|
698
|
+
result.unwrap_err(),
|
699
|
+
"test case: {name} - expected err"
|
700
|
+
);
|
701
|
+
} else {
|
702
|
+
assert!(result.is_ok(), "test case: {name} - success");
|
703
|
+
|
704
|
+
assert_eq!(value, result.unwrap(), "test case: {name} - value loaded");
|
705
|
+
}
|
706
|
+
}
|
707
|
+
}
|
708
|
+
}
|