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.
@@ -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
+ }