Skip to content

Commit

Permalink
Remove unreachable errors; add unit tests for coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
shutton committed Mar 11, 2024
1 parent 2894492 commit 2c72869
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 10 deletions.
16 changes: 16 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
fn main() -> Result<(), std::io::Error> {
let sample_table = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/sample-tables/sample-table.txt"
))?;

let sample_table = format!("const SAMPLE_TABLE: &str = {sample_table:?};\n");

let out_dir = std::env::var("OUT_DIR").expect("env OUT_DIR");
std::fs::write(
format!("{out_dir}/sample_table.rs"),
sample_table.as_bytes(),
)?;

Ok(())
}
106 changes: 97 additions & 9 deletions src/routing_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,10 @@ pub enum Error {
NetstatFail(ExitStatus),
#[error("netstat output not non-UTF-8")]
NetstatUtf8(FromUtf8Error),
#[error("Unrecognized section name {0:?}")]
NetstatParseUnrecognizedSectionName(String),
#[error("{0:?} section missing headers")]
#[error("no headers follow {0:?} section marker")]
NetstatParseNoHeaders(String),
#[error("parsing route entry: {0}")]
RouteEntryParse(#[from] crate::route_entry::Error),
#[error("CIDR first address neither V4 nor V6")]
CidrNotV4V6,
#[error("route entry found before protocol (Internet/Internet6) found.")]
EntryBeforeProto,
}
Expand Down Expand Up @@ -67,9 +63,7 @@ impl RoutingTable {
proto = match section {
"Internet:" => Some(Protocol::V4),
"Internet6:" => Some(Protocol::V6),
_ => {
return Err(Error::NetstatParseUnrecognizedSectionName(section.into()))
}
_ => unreachable!(),
};
// Next line will contain the column headers
if let Some(line) = lines.next() {
Expand All @@ -88,7 +82,9 @@ impl RoutingTable {
if cidr.is_host_address() {
let route = route.clone();
let gws = if_router.entry(route.net_if).or_insert_with(Vec::new);
gws.push(cidr.first_address().ok_or(Error::CidrNotV4V6)?);
// The route parser doesn't produce `Any` CIDRs,
// so there's always a first address.
gws.push(cidr.first_address().unwrap_or_else(|| unreachable!()));
}
}
routes.push(route);
Expand Down Expand Up @@ -123,6 +119,10 @@ impl RoutingTable {
}

/// Execute `netstat -rn` and return the output
///
/// # Errors
///
/// Returns an error if command execution fails, or the output is not UTF-8
pub async fn execute_netstat() -> Result<String, Error> {
let output = Command::new(NETSTAT_PATH)
.arg("-rn")
Expand All @@ -135,3 +135,91 @@ pub async fn execute_netstat() -> Result<String, Error> {
}
String::from_utf8(output.stdout).map_err(Error::NetstatUtf8)
}

#[cfg(test)]
mod tests {
use super::Error;
use crate::{Destination, Entity, RoutingTable};
use std::{process::ExitStatus, string::FromUtf8Error};

include!(concat!(env!("OUT_DIR"), "/sample_table.rs"));

#[tokio::test]
async fn coverage() {
let rt = RoutingTable::from_netstat_output(SAMPLE_TABLE).expect("parse routing table");
let _ = format!("{rt:?}");
let _ = format!(
"{:?}",
Error::NetstatExec(std::io::Error::from_raw_os_error(1))
);
let _ = format!("{:?}", Error::NetstatFail(ExitStatus::default()));
// This error is reachable only if the netstat command outputs invalid
// UTF-8.
let from_utf8err = String::from_utf8([0xa0, 0xa1].to_vec()).unwrap_err();
let _ = format!("{:?}", Error::NetstatUtf8(from_utf8err));
}

#[tokio::test]
#[cfg(target_os = "macos")]
async fn live_test() {
let _routing_table = RoutingTable::load_from_netstat()
.await
.expect("parse live routing table");
}

#[test]
fn good_table() {
let rt = RoutingTable::from_netstat_output(SAMPLE_TABLE).expect("parse routing table");
let entry = rt.find_route_entry("1.1.1.1".parse().unwrap());
dbg!(&entry);
assert!(entry.is_some());
let entry = entry.unwrap();
assert!(matches!(
entry.dest,
Destination {
entity: Entity::Default,
zone: None
}
));
// Coverage of debug formatting
let _ = format!("{rt:?}");
}

#[test]
fn missing_headers() {
for section in ["", "6"] {
let input = format!("{SAMPLE_TABLE}Internet{section}:\n");
let result = RoutingTable::from_netstat_output(&input);
assert!(matches!(result, Err(Error::NetstatParseNoHeaders(_))));
// Coverage of debug formatting
let _ = format!("{:?}", result.unwrap_err());
}
}

#[test]
fn stray_entry() {
let input = format!("extra stuff\n{SAMPLE_TABLE}");
let result = RoutingTable::from_netstat_output(&input);
assert!(matches!(result, Err(Error::EntryBeforeProto)));
// Coverage of debug formatting
let _ = format!("{:?}", result.unwrap_err());
}

#[test]
fn bad_entry() {
let input = format!("{SAMPLE_TABLE}How now brown cow.\n");
let result = RoutingTable::from_netstat_output(&input);
dbg!(&result);
assert!(matches!(
result,
Err(Error::RouteEntryParse(
crate::route_entry::Error::ParseIPv4AddrBadInt {
addr: _,
err: std::num::ParseIntError { .. },
}
))
));
// Coverage of debug formatting
let _ = format!("{:?}", result.unwrap_err());
}
}
2 changes: 1 addition & 1 deletion tests/live.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::missing_panics_doc, clippy::missing_errors_doc)]

use anyhow::Result;
use macos_routing_table::{execute_netstat, RoutingTable};
Expand Down

0 comments on commit 2c72869

Please sign in to comment.