From e29b4421befc2533ec694620dac1d0e9887a717a Mon Sep 17 00:00:00 2001 From: Mathias Hall-Andersen Date: Fri, 20 Sep 2019 12:34:20 +0200 Subject: [PATCH 1/2] Ported unit tests from WireGuard --- tests/tests.rs | 163 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/tests/tests.rs b/tests/tests.rs index 3ca3986..a7382dd 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -186,3 +186,166 @@ fn issue_7() { println!("len: {}", table.len()); } + +#[test] +fn ipv4_tests() { + let a = 1; + let b = 2; + let c = 3; + let d = 4; + let e = 5; + let g = 6; + let h = 7; + + let mut table: IpLookupTable = IpLookupTable::new(); + + macro_rules! insert { + ($v:expr, $o1:expr, $o2:expr, $o3:expr, $o4:expr, $cidr:expr) => { + table.insert(Ipv4Addr::new($o1, $o2, $o3, $o4), $cidr, $v); + }; + } + + macro_rules! assert_match { + ($v:expr, $o1:expr, $o2:expr, $o3:expr, $o4:expr) => { + let res = table + .longest_match(Ipv4Addr::new($o1, $o2, $o3, $o4)) + .unwrap(); + assert_eq!(*res.2, $v); + }; + } + + macro_rules! assert_not_match { + ($v:expr, $o1:expr, $o2:expr, $o3:expr, $o4:expr) => { + let res = table + .longest_match(Ipv4Addr::new($o1, $o2, $o3, $o4)) + .unwrap(); + assert_ne!(*res.2, $v); + }; + } + + macro_rules! remove_by_value { + ($v: expr) => { + let mut remove = vec![]; + for (ip, cidr, v) in table.iter_mut() { + if *v == $v { + remove.push((ip, cidr)); + } + } + for (ip, cidr) in remove { + table.remove(ip, cidr); + } + }; + } + + insert!(a, 192, 168, 4, 0, 24); + insert!(b, 192, 168, 4, 4, 32); + insert!(c, 192, 168, 0, 0, 16); + insert!(d, 192, 95, 5, 64, 27); + insert!(c, 192, 95, 5, 65, 27); + insert!(e, 0, 0, 0, 0, 0); + insert!(g, 64, 15, 112, 0, 20); + insert!(h, 64, 15, 123, 128, 25); + insert!(a, 10, 0, 0, 0, 25); + insert!(b, 10, 0, 0, 128, 25); + insert!(a, 10, 1, 0, 0, 30); + insert!(b, 10, 1, 0, 4, 30); + insert!(c, 10, 1, 0, 8, 29); + insert!(d, 10, 1, 0, 16, 29); + + assert_match!(a, 192, 168, 4, 20); + assert_match!(a, 192, 168, 4, 0); + assert_match!(b, 192, 168, 4, 4); + assert_match!(c, 192, 168, 200, 182); + assert_match!(c, 192, 95, 5, 68); + assert_match!(e, 192, 95, 5, 96); + assert_match!(g, 64, 15, 116, 26); + assert_match!(g, 64, 15, 127, 3); + + insert!(a, 1, 0, 0, 0, 32); + insert!(a, 64, 0, 0, 0, 32); + insert!(a, 128, 0, 0, 0, 32); + insert!(a, 192, 0, 0, 0, 32); + insert!(a, 255, 0, 0, 0, 32); + + assert_match!(a, 1, 0, 0, 0); + assert_match!(a, 64, 0, 0, 0); + assert_match!(a, 128, 0, 0, 0); + assert_match!(a, 192, 0, 0, 0); + assert_match!(a, 255, 0, 0, 0); + + remove_by_value!(a); + + assert_not_match!(a, 1, 0, 0, 0); + assert_not_match!(a, 64, 0, 0, 0); + assert_not_match!(a, 128, 0, 0, 0); + assert_not_match!(a, 192, 0, 0, 0); + assert_not_match!(a, 255, 0, 0, 0); +} + +#[test] +fn ipv6_tests() { + let a = 1; + let b = 2; + let c = 3; + let d = 4; + let e = 5; + let f = 6; + let g = 7; + let h = 8; + + let mut table: IpLookupTable = IpLookupTable::new(); + + macro_rules! ipv6 { + ($o1:expr, $o2:expr, $o3:expr, $o4:expr) => {{ + let o1: u32 = $o1; + let o2: u32 = $o2; + let o3: u32 = $o3; + let o4: u32 = $o4; + Ipv6Addr::new( + (o1 >> 16) as u16, + (o1 & 0xffff) as u16, + (o2 >> 16) as u16, + (o2 & 0xffff) as u16, + (o3 >> 16) as u16, + (o3 & 0xffff) as u16, + (o4 >> 16) as u16, + (o4 & 0xffff) as u16, + ) + }}; + } + + macro_rules! insert { + ($v:expr, $o1:expr, $o2:expr, $o3:expr, $o4:expr, $cidr:expr) => { + table.insert(ipv6!($o1, $o2, $o3, $o4), $cidr, $v); + }; + } + + macro_rules! assert_match { + ($v:expr, $o1:expr, $o2:expr, $o3:expr, $o4:expr) => { + let res = table.longest_match(ipv6!($o1, $o2, $o3, $o4)).unwrap(); + assert_eq!(*res.2, $v); + }; + } + + insert!(d, 0x26075300, 0x60006b00, 0, 0xc05f0543, 128); + insert!(c, 0x26075300, 0x60006b00, 0, 0, 64); + insert!(e, 0, 0, 0, 0, 0); + insert!(f, 0, 0, 0, 0, 0); + insert!(g, 0x24046800, 0, 0, 0, 32); + insert!(h, 0x24046800, 0x40040800, 0xdeadbeef, 0xdeadbeef, 64); + insert!(a, 0x24046800, 0x40040800, 0xdeadbeef, 0xdeadbeef, 128); + insert!(c, 0x24446800, 0x40e40800, 0xdeaebeef, 0xdefbeef, 128); + insert!(b, 0x24446800, 0xf0e40800, 0xeeaebeef, 0, 98); + + assert_match!(d, 0x26075300, 0x60006b00, 0, 0xc05f0543); + assert_match!(c, 0x26075300, 0x60006b00, 0, 0xc02e01ee); + assert_match!(f, 0x26075300, 0x60006b01, 0, 0); + assert_match!(g, 0x24046800, 0x40040806, 0, 0x1006); + assert_match!(g, 0x24046800, 0x40040806, 0x1234, 0x5678); + assert_match!(f, 0x240467ff, 0x40040806, 0x1234, 0x5678); + assert_match!(f, 0x24046801, 0x40040806, 0x1234, 0x5678); + assert_match!(h, 0x24046800, 0x40040800, 0x1234, 0x5678); + assert_match!(h, 0x24046800, 0x40040800, 0, 0); + assert_match!(h, 0x24046800, 0x40040800, 0x10101010, 0x10101010); + assert_match!(a, 0x24046800, 0x40040800, 0xdeadbeef, 0xdeadbeef); +} From f5e7d87775ba82fab3fe4210c42669227d8a33c9 Mon Sep 17 00:00:00 2001 From: Mathias Hall-Andersen Date: Fri, 20 Sep 2019 14:03:48 +0200 Subject: [PATCH 2/2] Added large randomized test Added a large randomized test for longest_match, by creating a naive implemenation using a linear scan of a list and comparing the behavior to the treebit map implemenation. --- Cargo.toml | 3 + tests/rand_test.rs | 209 +++++++++++++++++++++++++++++++++++++++++++++ tests/tests.rs | 2 + 3 files changed, 214 insertions(+) create mode 100644 tests/rand_test.rs diff --git a/Cargo.toml b/Cargo.toml index 9dc0f3d..d080096 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,9 @@ readme = "README.md" keywords = ["ip", "networking", "trie", "cidr"] categories = ["data-structures", "network-programming"] +[dev-dependencies] +rand = "0.7.2" + [badges] travis-ci = { repository = "hroi/treebitmap" } diff --git a/tests/rand_test.rs b/tests/rand_test.rs new file mode 100644 index 0000000..6e7e1d4 --- /dev/null +++ b/tests/rand_test.rs @@ -0,0 +1,209 @@ +extern crate rand; +extern crate treebitmap; + +use std::mem; +use std::net::{Ipv4Addr, Ipv6Addr}; + +use self::rand::{thread_rng, Rng}; + +use treebitmap::address::Address; +use treebitmap::*; + +const NUMBER_OF_ITERS: usize = 10; // number of times to run each test +const NUMBER_OF_PEERS: usize = 64; // number of distinct values +const NUMBER_OF_ADDRESS: usize = 100; // number of subnets in trie +const NUMBER_OF_LOOKUPS: usize = 1000; // number of lookups per test + +/* Randomized tests against a naive implemenation of a prefix trie. + */ + +struct SlowNode { + value: T, + ip: A, + masklen: u32, +} + +struct SlowRouter { + nodes: Vec>, +} + +impl SlowNode { + fn new(ip: A, masklen: u32, value: T) -> SlowNode { + SlowNode { ip, masklen, value } + } + + fn matches(&self, ip: A) -> bool { + let mut remain = self.masklen; + for (n1, n2) in ip + .nibbles() + .as_ref() + .iter() + .zip(self.ip.nibbles().as_ref().iter()) + { + let check = if remain > 4 { 4 } else { remain }; + let bits1 = n1 >> (4 - check); + let bits2 = n2 >> (4 - check); + if bits1 != bits2 { + return false; + } + remain -= check; + } + true + } + + fn in_range(&self, rng: &mut R) -> A + where + R: Rng, + { + // create mutable copy of nibs + let org = self.ip.nibbles(); + let mut nibs = vec![0u8; org.as_ref().len()]; + nibs.copy_from_slice(org.as_ref()); + + // flip bits after masklen + for i in 0..nibs.len() * 4 { + if i >= self.masklen as usize { + let idx = i % 4; + let bit: u8 = rng.gen(); + let bit = (bit & 0x80) >> 4; // top-most bit of a nibble + nibs[i / 4] ^= bit >> idx; + assert!(nibs[i / 4] < 16); + } + } + + // santify check + let addr = A::from_nibbles(nibs.as_ref()); + assert!(self.matches(addr), "generated IP should match subnet"); + addr + } +} + +impl PartialEq for SlowNode { + fn eq(&self, rhs: &SlowNode) -> bool { + self.ip.nibbles().as_ref() == rhs.ip.nibbles().as_ref() && self.masklen == rhs.masklen + } +} + +impl SlowRouter { + fn new() -> SlowRouter { + SlowRouter { nodes: vec![] } + } + + fn insert(&mut self, ip: A, masklen: u32, value: T) -> Option { + let n1 = SlowNode::new(ip, masklen, value); + for n2 in self.nodes.iter_mut() { + if n1 == *n2 { + let old = mem::replace(n2, n1); + return Some(old.value); + } + } + self.nodes.push(n1); + None + } + + fn longest_match(&self, ip: A) -> Option<(A, u32, &T)> { + let mut res: Option<(A, u32, &T)> = None; + for node in self.nodes.iter() { + if node.matches(ip) { + match res { + None => res = Some((node.ip, node.masklen, &node.value)), + Some((_, l, _)) => { + if l < node.masklen { + res = Some((node.ip, node.masklen, &node.value)); + } + } + } + } + } + res + } + + fn any(&self, rng: &mut R) -> A + where + R: Rng, + { + let idx = rng.gen_range(0, self.nodes.len()); + self.nodes[idx].in_range(rng) + } +} + +#[test] +fn ipv6_random_test() { + for _ in 0..NUMBER_OF_ITERS { + let mut tbl = IpLookupTable::new(); + let mut slw = SlowRouter::new(); + let mut rng = thread_rng(); + + macro_rules! ipv6 { + () => {{ + Ipv6Addr::new( + rng.gen(), + rng.gen(), + rng.gen(), + rng.gen(), + rng.gen(), + rng.gen(), + rng.gen(), + rng.gen(), + ) + }}; + } + + for _ in 0..NUMBER_OF_ADDRESS { + let masklen = rng.gen_range(0, 128); + let ip = ipv6!().mask(masklen); + let value = rng.gen_range(0, NUMBER_OF_PEERS); + tbl.insert(ip, masklen, value); + slw.insert(ip, masklen, value); + } + + for _ in 0..NUMBER_OF_LOOKUPS { + let ip = if rng.gen() { + slw.any(&mut rng) + } else { + ipv6!() + }; + assert_eq!( + tbl.longest_match(ip), + slw.longest_match(ip), + "naive list implemenation and trie does not agree" + ); + } + } +} + +#[test] +fn ipv4_random_test() { + for _ in 0..NUMBER_OF_ITERS { + let mut tbl = IpLookupTable::new(); + let mut slw = SlowRouter::new(); + let mut rng = thread_rng(); + + macro_rules! ipv4 { + () => {{ + Ipv4Addr::new(rng.gen(), rng.gen(), rng.gen(), rng.gen()) + }}; + } + + for _ in 0..NUMBER_OF_ADDRESS { + let masklen = rng.gen_range(0, 32); + let ip = ipv4!().mask(masklen); + let value = rng.gen_range(0, NUMBER_OF_PEERS); + tbl.insert(ip, masklen, value); + slw.insert(ip, masklen, value); + } + + for _ in 0..NUMBER_OF_LOOKUPS { + let ip = if rng.gen() { + slw.any(&mut rng) + } else { + ipv4!() + }; + assert_eq!( + tbl.longest_match(ip), + slw.longest_match(ip), + "naive list implemenation and trie does not agree" + ); + } + } +} diff --git a/tests/tests.rs b/tests/tests.rs index a7382dd..cc763fc 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -6,6 +6,8 @@ extern crate treebitmap; +mod rand_test; + use std::net::{Ipv4Addr, Ipv6Addr}; use std::str::FromStr; use treebitmap::*;