Skip to content

Commit 5488a11

Browse files
committed
Move tests into separate file, rename crate, add keywords
1 parent b07e56f commit 5488a11

File tree

5 files changed

+274
-192
lines changed

5 files changed

+274
-192
lines changed

Cargo.lock

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22
name = "sorted-index-buffer"
33
version = "0.1.0"
44
edition = "2024"
5+
readme = "README.md"
6+
description = "array based sorted map with u64 keys"
7+
license = "MIT OR Apache-2.0"
8+
authors = ["rklaehn <[email protected]>", "n0 team"]
9+
repository = "https://github.com/n0-computer/iroh"
10+
keywords = ["map", "buffer"]
11+
categories = ["data-structures"]
512

613
[dependencies]
714

815
[dev-dependencies]
916
proptest = "1.9.0"
1017
rand = "0.9.2"
18+
test-strategy = "0.4.3"

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# SortedIndexBuffer
2+
3+
This crate provides a data structure with identical behaviour to a `BTreeMap<u64, T>`,
4+
but optimized for the case where keys are mostly consecutive.
5+
6+
## License
7+
8+
Copyright 2025 N0, INC.
9+
10+
This project is licensed under either of
11+
12+
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
13+
http://www.apache.org/licenses/LICENSE-2.0)
14+
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
15+
http://opensource.org/licenses/MIT)
16+
17+
at your option.
18+
19+
## Contribution
20+
21+
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

src/lib.rs

Lines changed: 7 additions & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
#[cfg(test)]
2+
mod tests;
3+
14
/// A buffer indexed by a u64 sequence number.
25
///
36
/// This behaves idential to a BTreeMap<u64, T>, but is optimized for the case where
@@ -191,6 +194,10 @@ impl<T> SortedIndexBuffer<T> {
191194
}
192195

193196
/// Turn into an iterator over all (index, value) pairs in the buffer in ascending order of their keys.
197+
///
198+
/// This is an explicit method instead of implementing IntoIterator, so we can return a
199+
/// DoubleEndedIterator without having to name the iterator type.
200+
#[allow(clippy::should_implement_trait)]
194201
pub fn into_iter(self) -> impl DoubleEndedIterator<Item = (u64, T)> {
195202
let base = base(self.min, self.max);
196203
self.data
@@ -432,195 +439,3 @@ fn base(min: u64, max: u64) -> u64 {
432439
let mask = (buf_len as u64) / 2 - 1;
433440
min & !mask
434441
}
435-
436-
#[cfg(test)]
437-
mod tests {
438-
use std::collections::BTreeMap;
439-
440-
use proptest::prelude::*;
441-
442-
use super::*;
443-
444-
/// Randomly permutes an iterator such that each element is displaced by at most `k` positions.
445-
pub fn lag_permute<I: Iterator>(iter: I, k: usize) -> impl Iterator<Item = I::Item> {
446-
let mut source = iter;
447-
let mut buffer = Vec::with_capacity(k + 1);
448-
let mut rng = rand::rng();
449-
450-
std::iter::from_fn(move || {
451-
buffer.extend((&mut source).take(k + 1 - buffer.len()));
452-
453-
if buffer.is_empty() {
454-
return None;
455-
}
456-
457-
Some(buffer.swap_remove(rng.random_range(0..buffer.len())))
458-
})
459-
}
460-
461-
#[test]
462-
fn test_usage() {
463-
let elements = lag_permute(0..10000, 100).collect::<Vec<_>>();
464-
let mut reference = BTreeMap::<u64, u64>::new();
465-
let mut pb = SortedIndexBuffer::<u64>::default();
466-
let d = 100;
467-
let add = elements
468-
.iter()
469-
.map(|x| Some(*x))
470-
.chain(std::iter::repeat_n(None, d));
471-
let remove = std::iter::repeat_n(None, d).chain(elements.iter().map(|x| Some(*x)));
472-
for (a, r) in add.zip(remove) {
473-
if let Some(i) = a {
474-
pb.insert(i, i * 10);
475-
reference.insert(i, i * 10);
476-
}
477-
if let Some(i) = r {
478-
let v1 = pb.remove(i);
479-
let v2 = reference.remove(&i);
480-
assert_eq!(v1, v2);
481-
}
482-
assert_same(pb.iter(), reference.iter().map(|(k, v)| (*k, v)));
483-
pb.check_invariants_expensive();
484-
}
485-
assert!(reference.is_empty());
486-
assert!(pb.is_empty());
487-
}
488-
489-
#[test]
490-
fn test_range_iterators() {
491-
let mut pb = SortedIndexBuffer::default();
492-
for i in 0..100 {
493-
pb.insert(i, i * 10);
494-
}
495-
496-
// Test keys_range
497-
let keys: Vec<_> = pb.keys_range(10..20).collect();
498-
assert_eq!(keys, (10..20).collect::<Vec<_>>());
499-
500-
// Test reverse
501-
let keys_rev: Vec<_> = pb.keys_range(10..20).rev().collect();
502-
assert_eq!(keys_rev, (10..20).rev().collect::<Vec<_>>());
503-
504-
// Test values_range
505-
let values: Vec<_> = pb.values_range(10..20).cloned().collect();
506-
assert_eq!(values, (10..20).map(|i| i * 10).collect::<Vec<_>>());
507-
508-
// Test iter_range
509-
let pairs: Vec<_> = pb.iter_range(10..20).map(|(k, v)| (k, *v)).collect();
510-
assert_eq!(pairs, (10..20).map(|i| (i, i * 10)).collect::<Vec<_>>());
511-
}
512-
513-
#[test]
514-
fn test_retain() {
515-
let mut pb = SortedIndexBuffer::default();
516-
for i in 0..100 {
517-
pb.insert(i, i * 10);
518-
}
519-
520-
pb.retain(|i, _v| i % 2 == 0);
521-
522-
assert_eq!(pb.keys().next(), Some(0));
523-
assert_eq!(pb.keys().next_back(), Some(98));
524-
assert_eq!(pb.keys().count(), 50);
525-
for i in 0..100 {
526-
if i % 2 == 0 {
527-
assert_eq!(pb.get(i), Some(&(i * 10)));
528-
} else {
529-
assert_eq!(pb.get(i), None);
530-
}
531-
}
532-
pb.check_invariants_expensive();
533-
534-
pb.retain(|_, _| false);
535-
assert!(pb.is_empty());
536-
pb.check_invariants_expensive();
537-
}
538-
539-
#[test]
540-
fn test_retain_range() {
541-
let mut pb = SortedIndexBuffer::default();
542-
for i in 0..100 {
543-
pb.insert(i, i * 10);
544-
}
545-
546-
pb.retain_range(20..80);
547-
548-
assert_eq!(pb.keys().next(), Some(20));
549-
assert_eq!(pb.keys().next_back(), Some(79));
550-
assert_eq!(pb.keys().count(), 60);
551-
for i in 20..80 {
552-
assert_eq!(pb.get(i), Some(&(i * 10)));
553-
}
554-
pb.check_invariants_expensive();
555-
556-
// Retain with range outside current bounds -> empty
557-
pb.retain_range(200..300);
558-
assert!(pb.is_empty());
559-
pb.check_invariants_expensive();
560-
561-
// Rebuild and retain with superset range -> no-op
562-
for i in 10..20 {
563-
pb.insert(i, i * 10);
564-
}
565-
pb.retain_range(0..100);
566-
assert_eq!(pb.keys().count(), 10);
567-
pb.check_invariants_expensive();
568-
}
569-
570-
fn assert_same<I1, I2, T>(iter1: I1, iter2: I2)
571-
where
572-
I1: Iterator<Item = T>,
573-
I2: Iterator<Item = T>,
574-
T: PartialEq + std::fmt::Debug,
575-
{
576-
let vec1: Vec<T> = iter1.collect();
577-
let vec2: Vec<T> = iter2.collect();
578-
assert_eq!(vec1, vec2);
579-
}
580-
581-
#[derive(Debug, Clone)]
582-
enum InsertRemoveGetOp {
583-
Insert(u64, u64),
584-
Remove(u64),
585-
Get(u64),
586-
}
587-
588-
fn op_strategy() -> impl Strategy<Value = InsertRemoveGetOp> {
589-
prop_oneof![
590-
(0..1000u64, any::<u64>()).prop_map(|(k, v)| InsertRemoveGetOp::Insert(k, v)),
591-
(0..1000u64).prop_map(InsertRemoveGetOp::Remove),
592-
(0..1000u64).prop_map(InsertRemoveGetOp::Get),
593-
]
594-
}
595-
596-
proptest! {
597-
#[test]
598-
fn test_insert_remove_get(ops in prop::collection::vec(op_strategy(), 0..1000)) {
599-
let mut pb = SortedIndexBuffer::default();
600-
let mut reference = BTreeMap::new();
601-
602-
for op in ops {
603-
match op {
604-
InsertRemoveGetOp::Insert(k, v) => {
605-
pb.insert(k, v);
606-
reference.insert(k, v);
607-
}
608-
InsertRemoveGetOp::Remove(k) => {
609-
let v1 = pb.remove(k);
610-
let v2 = reference.remove(&k);
611-
assert_eq!(v1, v2);
612-
}
613-
InsertRemoveGetOp::Get(k) => {
614-
let v1 = pb.get(k);
615-
let v2 = reference.get(&k);
616-
assert_eq!(v1, v2);
617-
}
618-
}
619-
pb.check_invariants_expensive();
620-
}
621-
622-
// Final state should match
623-
assert_same(pb.iter(), reference.iter().map(|(k, v)| (*k, v)));
624-
}
625-
}
626-
}

0 commit comments

Comments
 (0)