Expand test coverage

This commit is contained in:
Juno Takano 2026-01-19 01:45:54 -03:00
commit 5151c53a2b
20 changed files with 773 additions and 121 deletions

View file

@ -263,7 +263,7 @@ export CARGO_TERM_COLOR := 'always'
debug_vars := 'DEBUG=${DEBUG:-} DEBUG_FILTER=${DEBUG_FILTER:-} RUST_BACKTRACE=${RUST_BACKTRACE:-} RUSTFLAGS=${RUSTFLAGS:-}'
watch_cmd := "watchexec -qc -r -e rs,toml,html --color always -- "
cover_cmd := 'cargo llvm-cov --color always --ignore-filename-regex "main\.rs|dev\.rs"'
cover_cmd := 'cargo llvm-cov --color always --ignore-filename-regex "main\.rs|log\.rs"'
just_cmd := 'just --timestamp --explain --command-color green'
set unstable

View file

@ -127,7 +127,6 @@ same_functions_in_if_condition = "warn"
semicolon_if_nothing_returned = "warn"
set_contains_or_insert = "warn"
should_panic_without_expect = "warn"
similar_names = "warn"
str_split_at_newline = "warn"
struct_field_names = "warn"
trivially_copy_pass_by_ref = "warn"

View file

@ -192,7 +192,7 @@ impl Graph {
tlog!(&instant, "Parsed configuration");
}
/// Construct a HashMap with incoming connections (reversed edges)
/// Construct a `HashMap` with incoming connections (reversed edges)
fn map_incoming(&mut self) {
for node in self.nodes.clone().into_values() {
for edge in node.connections.clone().values() {
@ -310,7 +310,6 @@ impl Graph {
let graph = self.clone();
let iterator = self.nodes.iter_mut();
for (key, node) in iterator {
// Parse node text
let parse_output = content::rich_parse(&node.text, &graph);
node.text
@ -504,7 +503,7 @@ impl std::fmt::Display for Format {
match self {
Format::TOML => write!(f, "TOML"),
Format::JSON => write!(f, "JSON"),
Format::Unsupported => write!(f, "Unsupported"),
Format::Unsupported => write!(f, "Unsupported format"),
}
}
}
@ -518,7 +517,15 @@ pub struct QueryResult {
impl std::fmt::Display for QueryResult {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let meta = if self.redirect { "[redirect] " } else { "" };
let meta = if self.redirect && self.exact {
"[exact redirect] "
} else if !self.redirect && self.exact {
"[exact] "
} else if self.redirect && !self.exact {
"[redirect] "
} else {
""
};
let node = if let Some(n) = &self.node {
n.id.clone()
} else {
@ -597,21 +604,30 @@ mod tests {
fn bad_deserial_input() {
let result = Graph::from_serial("not toml", &Format::TOML);
assert!(result.is_err());
assert!(matches!(result.unwrap_err().cause, SerialErrorCause::MalformedInput));
assert!(matches!(
result.unwrap_err().cause,
SerialErrorCause::MalformedInput
));
}
#[test]
fn bad_deserial_format() {
let result = Graph::from_serial("not toml", &Format::Unsupported);
assert!(result.is_err());
assert!(matches!(result.unwrap_err().cause, SerialErrorCause::UnsupportedFormat));
assert!(matches!(
result.unwrap_err().cause,
SerialErrorCause::UnsupportedFormat
));
}
#[test]
fn bad_serial_format() {
let result = Graph::load().to_serial(&Format::Unsupported);
assert!(result.is_err());
assert!(matches!(result.unwrap_err().cause, SerialErrorCause::UnsupportedFormat));
assert!(matches!(
result.unwrap_err().cause,
SerialErrorCause::UnsupportedFormat
));
}
#[test]
@ -625,12 +641,11 @@ mod tests {
#[test]
fn title_population_from_id() {
let mut graph = Graph::from_serial(concat!(
"[nodes.TitlelessNode]\n",
r#"text = "Some text""#,
),
let mut graph = Graph::from_serial(
concat!("[nodes.TitlelessNode]\n", r#"text = "Some text""#,),
&Format::TOML,
).unwrap();
)
.unwrap();
graph.modulate();
let node = graph.nodes.get("TitlelessNode");
@ -639,13 +654,16 @@ mod tests {
#[test]
fn no_title_population_from_id_if_title_set() {
let mut graph = Graph::from_serial(concat!(
"[nodes.TitlefulNode]\n",
r#"title = "A Title""#, "\n",
r#"text = "Some text""#,
let mut graph = Graph::from_serial(
concat!(
"[nodes.TitlefulNode]\n",
r#"title = "A Title""#,
"\n",
r#"text = "Some text""#,
),
&Format::TOML,
).unwrap();
)
.unwrap();
graph.modulate();
let node = graph.nodes.get("TitlefulNode");
@ -654,32 +672,38 @@ mod tests {
#[test]
fn detached_edge_is_flagged() {
let mut graph = Graph::from_serial(concat!(
"[nodes.Node]\n",
r#"text = "Some text here""#, "\n\n",
"[nodes.Node.connections.Nowhere]\n",
let mut graph = Graph::from_serial(
concat!(
"[nodes.Node]\n",
r#"text = "Some text here""#,
"\n\n",
"[nodes.Node.connections.Nowhere]\n",
),
&Format::TOML,
).unwrap();
)
.unwrap();
graph.modulate();
let node = graph.nodes.get("Node").unwrap();
let connections = node.connections.as_ref().unwrap();
let connection = connections.get("Nowhere").unwrap();
let connection = node.connections.get("Nowhere").unwrap();
assert!(connection.detached);
}
#[test]
fn attached_edge_is_not_flagged() {
let mut graph = Graph::from_serial(concat!(
"[nodes.NodeOne]\n",
r#"text = "Some text here""#, "\n\n",
"[nodes.NodeOne.connections.NodeTwo]\n\n",
"[nodes.NodeTwo]\n",
r#"text = "Some other text here""#, "\n\n",
let mut graph = Graph::from_serial(
concat!(
"[nodes.NodeOne]\n",
r#"text = "Some text here""#,
"\n\n",
"[nodes.NodeOne.connections.NodeTwo]\n\n",
"[nodes.NodeTwo]\n",
r#"text = "Some other text here""#,
"\n\n",
),
&Format::TOML,
).unwrap();
)
.unwrap();
graph.modulate();
let node = graph.nodes.get("NodeOne").unwrap();
@ -690,14 +714,16 @@ mod tests {
#[test]
fn to_and_from_population() {
let mut graph = Graph::from_serial(concat!(
"[nodes.n01]\n",
"[nodes.n01.connections.n02]\n\n",
"[nodes.n02]\n",
"[nodes.n02.connections.n03]\n\n",
let mut graph = Graph::from_serial(
concat!(
"[nodes.n01]\n",
"[nodes.n01.connections.n02]\n\n",
"[nodes.n02]\n",
"[nodes.n02.connections.n03]\n\n",
),
&Format::TOML,
).unwrap();
)
.unwrap();
graph.modulate();
let n01 = graph.nodes.get("n01").unwrap();
@ -715,15 +741,19 @@ mod tests {
#[test]
fn links_become_connections() {
let mut graph = Graph::from_serial(concat!(
"[nodes.n01]\n",
r#"links = [ "n02", "n03", "n04" ]"#, "\n\n",
"[nodes.n02]\n",
"[nodes.n04]\n",
r#"links = [ "n01", "n03" ]"#, "\n\n",
let mut graph = Graph::from_serial(
concat!(
"[nodes.n01]\n",
r#"links = [ "n02", "n03", "n04" ]"#,
"\n\n",
"[nodes.n02]\n",
"[nodes.n04]\n",
r#"links = [ "n01", "n03" ]"#,
"\n\n",
),
&Format::TOML,
).unwrap();
)
.unwrap();
graph.modulate();
let n01 = graph.nodes.get("n01").unwrap();
@ -762,14 +792,19 @@ mod tests {
#[test]
fn detached_count_increments() {
let mut graph = Graph::from_serial(concat!(
"[nodes.n01]\n",
r#"links = [ "n02", "n03", "n04", "n05", "n06", "n10" ]"#, "\n\n",
"[nodes.n02]\n",
"[nodes.n04]\n",
r#"links = [ "n01", "n02", "n03", "n06", "n11", "n15" ]"#, "\n\n"),
let mut graph = Graph::from_serial(
concat!(
"[nodes.n01]\n",
r#"links = [ "n02", "n03", "n04", "n05", "n06", "n10" ]"#,
"\n\n",
"[nodes.n02]\n",
"[nodes.n04]\n",
r#"links = [ "n01", "n02", "n03", "n06", "n11", "n15" ]"#,
"\n\n"
),
&Format::TOML,
).unwrap();
)
.unwrap();
graph.modulate();
assert_eq!(graph.stats.detached_total, 8);
@ -778,12 +813,15 @@ mod tests {
#[test]
fn populated_summary() {
let text = "vh18qEUN22X2SxLj6lpOOzMBB4N6S0UG";
let mut graph = Graph::from_serial(&format!(
"[nodes.n01]\n\
let mut graph = Graph::from_serial(
&format!(
"[nodes.n01]\n\
text = \"{text}\"\n\
"),
"
),
&Format::TOML,
).unwrap();
)
.unwrap();
graph.modulate();
assert!(graph.nodes.get("n01").unwrap().summary.contains(text));
@ -794,14 +832,16 @@ mod tests {
fn supplied_summary() {
let text = "vh18qEUN22X2SxLj6lpOOzMBB4N6S0UG";
let summary = "W5dhPgNs7S1Zsq6uPK47MAw8xXyNxwep";
let mut graph = Graph::from_serial(&format!(
"[nodes.n01]\n\
let mut graph = Graph::from_serial(
&format!(
"[nodes.n01]\n\
summary = \"{summary}\"\n\
text = \"{text}\"\n\
",
),
&Format::TOML,
).unwrap();
)
.unwrap();
graph.modulate();
assert_eq!(graph.nodes.get("n01").unwrap().summary, summary);
@ -816,10 +856,11 @@ mod tests {
6FokUX o OCEc LzZFfR1nkqa hWIF LdrtD3G. PDQwv Ba2PnZ yEBVpqQdt\n\n\
Py6aoPK FV7iU UdrYB vD UeMvvg u 5kbt 9ZW9x7MR"
);
let mut graph = Graph::from_serial(&format!(
"[nodes.n01]\ntext = \"\"\"{text}\"\"\"\n"),
let mut graph = Graph::from_serial(
&format!("[nodes.n01]\ntext = \"\"\"{text}\"\"\"\n"),
&Format::TOML,
).unwrap();
)
.unwrap();
graph.modulate();
assert_eq!(graph.nodes.get("n01").unwrap().summary, first_sentence);
@ -837,7 +878,8 @@ mod tests {
let mut graph = Graph::from_serial(
format!("[nodes.n01]\ntext = \"\"\"{text}\"\"\"\n").as_str(),
&Format::TOML,
).unwrap();
)
.unwrap();
graph.modulate();
assert_eq!(graph.nodes.get("n01").unwrap().summary, first_paragraph);
@ -845,23 +887,28 @@ mod tests {
#[test]
fn summary_from_first_300_chars() {
let first_300 = concat!("Primis, quod, cum in rerum natura duo ",
let first_300 = concat!(
"Primis, quod, cum in rerum natura duo ",
"quaerenda sint, unum, quae materia sit, ex qua quaeque res ",
"efficiatur, alterum, quae naturales essent nec tamen id, cuius ",
"causa haec finxerat, assecutus est: Nam si omnes veri erunt, ut ",
"Epicuri ratio docet, tum denique poterit aliquid cognosci et ",
"percipi? Quos q");
let tail = concat!("uam autem et praeterita grate meminit et ",
"percipi? Quos q"
);
let tail = concat!(
"uam autem et praeterita grate meminit et ",
"praesentibus ita potitur, ut animadvertat quanta sint ea ",
"quamque iucunda, neque pendet ex futuris, sed expectat illa, ",
"fruitur praesentibus ab iisque vitii");
"fruitur praesentibus ab iisque vitii"
);
let text = format!("{first_300}{tail}");
let summary = format!("{first_300}");
let mut graph = Graph::from_serial(
format!("[nodes.n01]\ntext = \"\"\"{text}\"\"\"\n").as_str(),
&Format::TOML,
).unwrap();
)
.unwrap();
graph.modulate();
assert_eq!(graph.nodes.get("n01").unwrap().summary, summary);
@ -869,16 +916,18 @@ mod tests {
#[test]
fn anchors_become_connections() {
let mut graph = Graph::from_serial("\
let mut graph = Graph::from_serial(
"\
[nodes.n1]
text = 'an anchor to |n2|, the existing node'
[nodes.n2]
text = 'an anchor to |n0|, the nonexistent node'
", &Format::TOML,
).unwrap();
",
&Format::TOML,
)
.unwrap();
graph.modulate();
let n1_to_n2 = graph.nodes.get("n1").unwrap().connections.get("n2");
@ -891,6 +940,334 @@ mod tests {
assert!(!n1_to_n2.unwrap().detached);
assert!(n2_to_n0.unwrap().detached);
}
#[test]
fn detached_anchors_increase_counts() {
let mut graph = Graph::from_serial(
"\
[nodes.n1]\n\
text = 'this |anchor| is detached, as is |this one|.'\n\
",
&Format::TOML,
)
.unwrap();
graph.modulate();
assert_eq!(graph.stats.detached_total, 2);
assert_eq!(*graph.stats.detached.get("anchor").unwrap(), 1);
assert_eq!(*graph.stats.detached.get("this one").unwrap(), 1);
}
#[test]
fn repeated_detached_anchors_increase_counts() {
let mut graph = Graph::from_serial(
"\
[nodes.n1]\n\
text = 'this |anchor| is detached, as appears twice: |anchor|.'\n\
",
&Format::TOML,
)
.unwrap();
graph.modulate();
assert_eq!(graph.stats.detached_total, 2);
assert_eq!(*graph.stats.detached.get("anchor").unwrap(), 2);
}
#[test]
fn find_exact() {
let mut graph = Graph::from_serial(
"\
[nodes.n1]\n\
",
&Format::TOML,
)
.unwrap();
graph.modulate();
let query_result = graph.find_node("n1");
assert_eq!(query_result.node.unwrap().id, "n1");
assert!(query_result.exact);
assert!(!query_result.redirect);
}
#[test]
fn find_inexact() {
let mut graph = Graph::from_serial(
"\
[nodes.n1]\n\
",
&Format::TOML,
)
.unwrap();
graph.modulate();
let query_result = graph.find_node("n 1");
println!("{query_result}");
assert_eq!(query_result.node.unwrap().id, "n1");
assert!(!query_result.exact);
assert!(!query_result.redirect);
}
#[test]
fn find_inexact_to_redirect() {
let mut graph = Graph::from_serial(
"\
[nodes.n1]\n\
redirect = 'n3'
\n\
[nodes.n3]
",
&Format::TOML,
)
.unwrap();
graph.modulate();
let query_result = graph.find_node("n 1");
println!("{query_result}");
assert_eq!(query_result.node.unwrap().id, "n3");
assert!(!query_result.exact);
assert!(query_result.redirect);
}
#[test]
fn double_recursion_to_redirect() {
let mut graph = Graph::from_serial(
"\
[nodes.n1]\n\
redirect = 'n2'
\n\
[nodes.n2]\n\
redirect = 'n 3'
\n\
[nodes.n3]
",
&Format::TOML,
)
.unwrap();
graph.modulate();
let query_result = graph.find_node("n 1");
println!("{query_result}");
assert_eq!(query_result.node.unwrap().id, "n3");
assert!(!query_result.exact);
assert!(query_result.redirect);
}
#[test]
fn find_redirect_to_inexisting() {
let mut graph = Graph::from_serial(
"\
[nodes.n1]\n\
redirect = 'n0'\n\
",
&Format::TOML,
)
.unwrap();
graph.modulate();
let query_result = graph.find_node("n1");
println!("{query_result}");
assert!(query_result.node.is_none());
assert!(query_result.redirect);
assert!(!query_result.exact);
}
#[test]
fn find_redirect_to_existing() {
let mut graph = Graph::from_serial(
"\
[nodes.n1]\n\
redirect = 'n2'\n\
\n\
[nodes.n2]
",
&Format::TOML,
)
.unwrap();
graph.modulate();
let query_result = graph.find_node("n1");
assert_eq!(query_result.node.unwrap().id, "n2");
assert!(query_result.redirect);
assert!(!query_result.exact);
}
#[test]
fn serial_error_display() {
let bad_input = SerialErrorCause::MalformedInput;
let bad_format = SerialErrorCause::UnsupportedFormat;
assert_eq!(format!("{bad_input}"), "Malformed Input");
assert_eq!(format!("{bad_format}"), "Unsupported Format");
}
#[test]
fn string_from_serial_error() {
let bad_input_error_message = "denSehpfhCjr05gUd7TgYLb8veJHAMZW";
let bad_input_cause = SerialErrorCause::MalformedInput;
let bad_input = SerialError {
cause: bad_input_cause.clone(),
message: bad_input_error_message.to_string(),
};
let bad_format_error_message = "4brcCkWOgLHBvhLk2OcgTOKQgpKrc1bB";
let bad_format_cause = SerialErrorCause::UnsupportedFormat;
let bad_format = SerialError {
cause: bad_format_cause.clone(),
message: bad_format_error_message.to_string(),
};
let s_bad_input = String::from(bad_input.clone());
let s_bad_format = String::from(bad_format.clone());
assert!(s_bad_input.contains(bad_input_error_message));
assert!(s_bad_input.contains(bad_input.message.as_str()));
assert!(s_bad_input.contains(format!("{bad_input_cause}").as_str()));
assert!(s_bad_format.contains(bad_format_error_message));
assert!(s_bad_format.contains(bad_format.message.as_str()));
assert!(s_bad_format.contains(format!("{bad_format_cause}").as_str()));
}
#[test]
fn format_from_str() {
let uppercase_toml = Format::from("TOML");
let lowercase_toml = Format::from("toml");
let mixed_case_toml = Format::from("tOmL");
assert!(matches!(uppercase_toml, Format::TOML));
assert!(matches!(lowercase_toml, Format::TOML));
assert!(matches!(mixed_case_toml, Format::TOML));
let uppercase_json = Format::from("JSON");
let lowercase_json = Format::from("json");
let mixed_case_json = Format::from("JsoN");
assert!(matches!(uppercase_json, Format::JSON));
assert!(matches!(lowercase_json, Format::JSON));
assert!(matches!(mixed_case_json, Format::JSON));
let unsupported = [
Format::from("j son"),
Format::from(""),
Format::from("strawberry"),
Format::from(" "),
Format::from("\n"),
];
assert!(unsupported.iter().all(|f| matches!(f, Format::Unsupported)));
}
#[test]
fn format_display() {
let toml = format!("{}", Format::TOML);
let json = format!("{}", Format::JSON);
let unsupported = format!("{}", Format::Unsupported);
assert_eq!(toml, "TOML");
assert_eq!(json, "JSON");
assert_eq!(unsupported, "Unsupported format");
}
#[test]
fn query_result_display() {
let mut node = Node::default();
let node_id = "nv00qmO6PDrqJheUHOONlCVpuceefS30";
node.id = String::from(node_id);
let none_exact = QueryResult {
node: None,
exact: true,
redirect: false,
};
assert!(!format!("{none_exact}").contains(node_id));
assert!(format!("{none_exact}").contains("No Match"));
assert!(format!("{none_exact}").contains("exact"));
assert!(!format!("{none_exact}").contains("redirect"));
let some_exact = QueryResult {
node: Some(node.clone()),
exact: true,
redirect: false,
};
assert!(format!("{some_exact}").contains(node_id));
assert!(!format!("{some_exact}").contains("No Match"));
assert!(format!("{some_exact}").contains("exact"));
assert!(!format!("{some_exact}").contains("redirect"));
let none_redirect = QueryResult {
node: None,
exact: false,
redirect: true,
};
assert!(!format!("{none_redirect}").contains(node_id));
assert!(format!("{none_redirect}").contains("No Match"));
assert!(format!("{none_redirect}").contains("redirect"));
assert!(!format!("{none_redirect}").contains("exact"));
let some_redirect = QueryResult {
node: Some(node.clone()),
exact: false,
redirect: true,
};
assert!(format!("{some_redirect}").contains(node_id));
assert!(!format!("{some_redirect}").contains("No Match"));
assert!(format!("{some_redirect}").contains("redirect"));
assert!(!format!("{some_redirect}").contains("exact"));
let some_exact_redirect = QueryResult {
node: Some(node.clone()),
exact: true,
redirect: true,
};
assert!(format!("{some_exact_redirect}").contains(node_id));
assert!(!format!("{some_exact_redirect}").contains("No Match"));
assert!(format!("{some_exact_redirect}").contains("redirect"));
assert!(format!("{some_exact_redirect}").contains("exact"));
let none_exact_redirect = QueryResult {
node: None,
exact: true,
redirect: true,
};
assert!(!format!("{none_exact_redirect}").contains(node_id));
assert!(format!("{none_exact_redirect}").contains("No Match"));
assert!(format!("{none_exact_redirect}").contains("redirect"));
assert!(format!("{none_exact_redirect}").contains("exact"));
let none = QueryResult {
node: None,
exact: false,
redirect: false,
};
assert!(!format!("{none}").contains(node_id));
assert!(format!("{none}").contains("No Match"));
assert!(!format!("{none}").contains("redirect"));
assert!(!format!("{none}").contains("exact"));
let some = QueryResult {
node: Some(node.clone()),
exact: false,
redirect: false,
};
assert!(format!("{some}").contains(node_id));
assert!(!format!("{some}").contains("No Match"));
assert!(!format!("{some}").contains("redirect"));
assert!(!format!("{some}").contains("exact"));
}
}
#[cfg(test)]

View file

@ -59,28 +59,26 @@ impl From<u16> for Level {
impl From<&str> for Level {
fn from(s: &str) -> Level {
if s == "0" || s == "SILENT" || s == "silent" {
if s == "0" || s.to_uppercase() == "SILENT" {
Level::SILENT
} else if s == "1" || s == "FATAL" || s == "fatal" {
} else if s == "1" || s.to_uppercase() == "FATAL" {
Level::FATAL
} else if s == "2" || s == "ERROR" || s == "error" {
} else if s == "2" || s.to_uppercase() == "ERROR" {
Level::ERROR
} else if s == "3"
|| s == "WARN"
|| s == "warn"
|| s == "WARNING"
|| s == "warning"
|| s.to_uppercase() == "WARN"
|| s.to_uppercase() == "WARNING"
{
Level::WARN
} else if s == "4" || s == "INFO" || s == "info" {
} else if s == "4" || s.to_uppercase() == "INFO" {
Level::INFO
} else if s == "5" || s == "DEBUG" || s == "debug" {
} else if s == "5" || s.to_uppercase() == "DEBUG" {
Level::DEBUG
} else if s == "6" || s == "VERBOSE" || s == "verbose" {
} else if s == "6" || s.to_uppercase() == "VERBOSE" {
Level::VERBOSE
} else if s == "7" || s == "TRACE" || s == "trace" {
} else if s == "7" || s.to_uppercase() == "TRACE" {
Level::TRACE
} else if s == "37" || s == "META" || s == "meta" {
} else if s == "37" || s.to_uppercase() == "META" {
Level::META
} else {
super::ENV_DEFAULT
@ -103,3 +101,94 @@ impl std::fmt::Display for Level {
write!(f, "{s}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn u16_from_level() {
assert_eq!(u16::from(Level::SILENT), 0);
assert_eq!(u16::from(Level::FATAL), 1);
assert_eq!(u16::from(Level::ERROR), 2);
assert_eq!(u16::from(Level::WARN), 3);
assert_eq!(u16::from(Level::INFO), 4);
assert_eq!(u16::from(Level::DEBUG), 5);
assert_eq!(u16::from(Level::VERBOSE), 6);
assert_eq!(u16::from(Level::TRACE), 7);
assert_eq!(u16::from(Level::META), 37);
}
#[test]
fn level_from_u16() {
assert_eq!(Level::from(0), Level::SILENT);
assert_eq!(Level::from(1), Level::FATAL);
assert_eq!(Level::from(2), Level::ERROR);
assert_eq!(Level::from(3), Level::WARN);
assert_eq!(Level::from(4), Level::INFO);
assert_eq!(Level::from(5), Level::DEBUG);
assert_eq!(Level::from(6), Level::VERBOSE);
assert_eq!(Level::from(7), Level::TRACE);
assert_eq!(Level::from(37), Level::META);
assert_eq!(Level::from(99), Level::WARN);
}
#[test]
fn level_from_str() {
assert_eq!(Level::from("0"), Level::SILENT);
assert_eq!(Level::from("SILENT"), Level::SILENT);
assert_eq!(Level::from("silent"), Level::SILENT);
assert_eq!(Level::from("SiLEnT"), Level::SILENT);
assert_eq!(Level::from("1"), Level::FATAL);
assert_eq!(Level::from("FATAL"), Level::FATAL);
assert_eq!(Level::from("fatal"), Level::FATAL);
assert_eq!(Level::from("FaTaL"), Level::FATAL);
assert_eq!(Level::from("3"), Level::WARN);
assert_eq!(Level::from("WARN"), Level::WARN);
assert_eq!(Level::from("warn"), Level::WARN);
assert_eq!(Level::from("WaRn"), Level::WARN);
assert_eq!(Level::from("WARNING"), Level::WARN);
assert_eq!(Level::from("warning"), Level::WARN);
assert_eq!(Level::from("WaRninG"), Level::WARN);
assert_eq!(Level::from("4"), Level::INFO);
assert_eq!(Level::from("INFO"), Level::INFO);
assert_eq!(Level::from("info"), Level::INFO);
assert_eq!(Level::from("iNFo"), Level::INFO);
assert_eq!(Level::from("5"), Level::DEBUG);
assert_eq!(Level::from("DEBUG"), Level::DEBUG);
assert_eq!(Level::from("debug"), Level::DEBUG);
assert_eq!(Level::from("deBuG"), Level::DEBUG);
assert_eq!(Level::from("6"), Level::VERBOSE);
assert_eq!(Level::from("VERBOSE"), Level::VERBOSE);
assert_eq!(Level::from("verbose"), Level::VERBOSE);
assert_eq!(Level::from("VerBosE"), Level::VERBOSE);
assert_eq!(Level::from("7"), Level::TRACE);
assert_eq!(Level::from("TRACE"), Level::TRACE);
assert_eq!(Level::from("trace"), Level::TRACE);
assert_eq!(Level::from("trAcE"), Level::TRACE);
assert_eq!(Level::from("37"), Level::META);
assert_eq!(Level::from("META"), Level::META);
assert_eq!(Level::from("meta"), Level::META);
assert_eq!(Level::from("mETa"), Level::META);
}
#[test]
fn display_level() {
assert_eq!(format!("{}", Level::SILENT), "SILENT");
assert_eq!(format!("{}", Level::FATAL), "FATAL");
assert_eq!(format!("{}", Level::ERROR), "ERROR");
assert_eq!(format!("{}", Level::WARN), "WARNING");
assert_eq!(format!("{}", Level::INFO), "INFO");
assert_eq!(format!("{}", Level::DEBUG), "DEBUG");
assert_eq!(format!("{}", Level::VERBOSE), "VERBOSE");
assert_eq!(format!("{}", Level::TRACE), "TRACE");
assert_eq!(format!("{}", Level::META), "META");
}
}

View file

@ -168,4 +168,13 @@ mod tests {
== "application/json"
);
}
#[tokio::test]
async fn not_found() {
let state = GlobalState {
graph: Graph::default(),
};
let response = file(Path("/k/j/m".to_string()), State(state)).await;
assert!(response.status() == StatusCode::NOT_FOUND);
}
}

View file

@ -17,18 +17,11 @@ pub async fn node(
let instant = now();
let result = state.graph.find_node(&id);
let found = result.node.is_some();
let node = result
.node
.unwrap_or(Node::new(Some(format!("Could not find node ID {id}."))));
let node = result.node.unwrap_or(Node::not_found(Some(format!(
"Could not find node ID {id}."
))));
if !node.redirect.is_empty() {
return Redirect::permanent(
format!("/node/{}", node.redirect).as_str(),
)
.into_response();
}
if found && !result.exact {
if found && (!result.exact || result.redirect) {
return Redirect::permanent(format!("/node/{}", node.id).as_str())
.into_response();
}
@ -60,7 +53,7 @@ mod tests {
http::{HeaderName, StatusCode},
};
use crate::graph::Graph;
use crate::graph::{Format, Graph};
use super::*;
@ -108,4 +101,21 @@ mod tests {
let response = wrap_node("docs").await;
assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT);
}
#[tokio::test]
async fn standalone_graph_redirect() {
let toml = "\
[nodes.n1]\n\
redirect = 'n2'\n\
\n\
[nodes.n2]\n\
text = 'n2 text'\n\
";
let graph = Graph::from_serial(toml, &Format::TOML).unwrap();
let state = GlobalState { graph };
let response =
node(Path("n1".to_string()), axum::extract::State(state)).await;
assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT);
}
}

View file

@ -92,3 +92,52 @@ impl Mime {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn smoke() {
let m = Mime::guess("/home/jane/top/inner/kitty.png");
assert_eq!(String::from(m), "image/png");
}
#[test]
fn all() {
let pairs = [
("file.txt", "text/plain"),
("file.csv", "text/csv"),
("file.css", "text/css"),
("file.ttf", "font/ttf"),
("file.otf", "font/otf"),
("file.woff", "font/woff"),
("file.woff2", "font/woff2"),
("file.svg", "image/svg+xml"),
("file.ico", "image/x-icon"),
("file.jpeg", "image/jpeg"),
("file.png", "image/png"),
("file.apng", "image/apng"),
("caddy.gif", "image/gif"),
("file.webp", "image/webp"),
("file.avif", "image/avif"),
("file.toml", "application/toml"),
("file.xml", "application/xml"),
("file.json", "application/json"),
("file.js", "text/javascript"),
("file.pdf", "application/pdf"),
("book.epub", "application/epub+zip"),
("weird.xzx", "application/octet-stream"),
];
for (file, mime) in pairs {
assert_eq!(String::from(Mime::guess(file)), mime);
}
}
#[test]
fn unknown() {
let u = Mime::guess("x");
assert!(matches!(u, Mime::Unknown));
}
}

View file

@ -1,3 +1,5 @@
use std::mem::discriminant;
use parser::{Token, Lexeme};
use crate::graph::Graph;
@ -21,6 +23,26 @@ pub struct TokenOutput {
pub format_tokens: Vec<Token>,
}
impl TokenOutput {
pub fn only(&self, kind: &Token) -> Vec<Token> {
let filter = |tokens: &[Token], k: &Token| -> Vec<Token> {
tokens
.iter()
.filter(|&t| discriminant(t) == discriminant(k))
.cloned()
.collect::<Vec<Token>>()
};
let filtered_tokens = filter(&self.tokens, kind);
let filtered_format_tokens = filter(&self.format_tokens, kind);
[filtered_tokens, filtered_format_tokens]
.into_iter()
.flatten()
.collect::<Vec<Token>>()
}
}
pub fn parse(text: &str, graph: &Graph) -> String {
parser::read(text, graph)
}
@ -28,3 +50,21 @@ pub fn parse(text: &str, graph: &Graph) -> String {
pub fn rich_parse(text: &str, graph: &Graph) -> TokenOutput {
parser::rich_read(text, graph)
}
#[cfg(test)]
mod tests {
use crate::syntax::content::parser::token::{Bold, Oblique};
use super::*;
#[test]
fn only() {
let graph = Graph::default();
let output = rich_parse("*four* *bold* and _two_ italic", &graph);
let bold_tokens = output.only(&Token::Bold(Bold::new(true)));
let italic_tokens = output.only(&Token::Oblique(Oblique::new(true)));
println!("{bold_tokens:?}");
assert_eq!(bold_tokens.len(), 4);
assert_eq!(italic_tokens.len(), 2);
}
}

View file

@ -1,4 +1,4 @@
use crate::syntax::content::Parseable as _;
use crate::syntax::content::Parseable;
pub mod anchor;
pub mod bold;

View file

@ -15,29 +15,6 @@ pub struct Anchor {
}
impl Anchor {
pub fn new(
text: &str,
destination: &str,
node: Option<Node>,
node_id: Option<String>,
leading: bool,
external: bool,
balanced: bool,
) -> Anchor {
let mut anchor = Anchor {
text: text.to_owned(),
destination: Some(String::from(destination)),
node,
node_id,
leading,
external,
balanced,
};
anchor.route();
anchor
}
pub fn text(&self) -> String {
self.text.clone()
}
@ -287,4 +264,18 @@ mod tests {
anchor.route(); // set_destination also called this
assert!(anchor.destination().is_none());
}
#[test]
fn set_node_id() {
let payload = "kxBDJ0EoDVaygxpZ8NgNdQrUIBsGimTs";
let mut anchor = Anchor::default();
anchor.set_node_id(payload);
assert_eq!(anchor.node_id.unwrap(), payload);
}
#[test]
fn display_no_destination() {
let anchor = Anchor::default();
assert_eq!(format!("{anchor}"), "Anchor <empty> -> <unknown>");
}
}

View file

@ -76,4 +76,10 @@ mod tests {
"Tk:Bold [closed]"
);
}
#[test]
fn flatten() {
let bold = Bold::new(false);
assert_eq!(bold.flatten(), "");
}
}

View file

@ -75,4 +75,10 @@ mod tests {
"Tk:CheckBox [empty]"
);
}
#[test]
fn flatten() {
let checkbox = CheckBox::new(false);
assert_eq!(checkbox.flatten(), "");
}
}

View file

@ -300,4 +300,30 @@ mod tests {
"Tk:Header [closed L2]"
);
}
#[test]
fn display_unknown_open_state() {
let header = Header {
open: None,
dom_id: None,
level: Level::One,
};
assert_eq!(format!("{header}"), "Header [unknown L1]");
}
#[test]
fn display_dom_id() {
let payload = "WIV0h1wCeY7Pp3FkjgIftJHX1I6YvnSc";
let header = Header {
open: None,
dom_id: Some(payload.to_string()),
level: Level::One,
};
assert_eq!(
format!("{header}"),
format!("Header [unknown L1 DOM ID {payload}]")
);
}
}

View file

@ -89,4 +89,10 @@ mod tests {
"Tk:Item [<unknown>] dRMy4"
);
}
#[test]
fn flatten() {
let item = Item::new("", None);
assert_eq!(item.flatten(), "");
}
}

View file

@ -189,4 +189,23 @@ mod tests {
let lexeme = Lexeme::new("SL6PX", "6xsNB", "oeAHa");
List::lex(&lexeme);
}
#[test]
fn ordered_list() {
let mut list = List::new(true);
list.items = vec![
Item::new("a", Some(0)),
Item::new("b", Some(0)),
Item::new("c", Some(0)),
];
assert_eq!(
list.render(),
"\n<ol>\n\
<li>a</li>\n\
<li>b</li>\n\
<li>c</li>\n\
</ol>\n\n"
);
}
}

View file

@ -79,4 +79,10 @@ mod tests {
"Tk:Oblique [closed]"
);
}
#[test]
fn flatten() {
let oblique = Oblique::new(false);
assert_eq!(oblique.flatten(), "");
}
}

View file

@ -99,4 +99,10 @@ mod tests {
"Tk:PreFormat [unknown]"
);
}
#[test]
fn flatten() {
let preformat = PreFormat::new(false);
assert_eq!(preformat.flatten(), "");
}
}

View file

@ -76,4 +76,10 @@ mod tests {
"Tk:Strike [closed]"
);
}
#[test]
fn flatten() {
let strike = Strike::new(false);
assert_eq!(strike.flatten(), "");
}
}

View file

@ -79,4 +79,10 @@ mod tests {
"Tk:Underline [closed]"
);
}
#[test]
fn flatten() {
let underline = Underline::new(false);
assert_eq!(underline.flatten(), "");
}
}

View file

@ -320,6 +320,7 @@ td, th {
summary {
margin-bottom: 15px;
cursor: pointer;
}
hr {