From 5151c53a2bc2fface99ee6dde7011aef497fae04 Mon Sep 17 00:00:00 2001 From: jutty Date: Mon, 19 Jan 2026 01:45:54 -0300 Subject: [PATCH] Expand test coverage --- .justfile | 2 +- Cargo.toml | 1 - src/graph.rs | 519 ++++++++++++++++--- src/log/level.rs | 113 +++- src/router/handlers/fixed.rs | 9 + src/router/handlers/graph.rs | 34 +- src/router/handlers/mime.rs | 49 ++ src/syntax/content.rs | 40 ++ src/syntax/content/parser/token.rs | 2 +- src/syntax/content/parser/token/anchor.rs | 37 +- src/syntax/content/parser/token/bold.rs | 6 + src/syntax/content/parser/token/checkbox.rs | 6 + src/syntax/content/parser/token/header.rs | 26 + src/syntax/content/parser/token/item.rs | 6 + src/syntax/content/parser/token/list.rs | 19 + src/syntax/content/parser/token/oblique.rs | 6 + src/syntax/content/parser/token/preformat.rs | 6 + src/syntax/content/parser/token/strike.rs | 6 + src/syntax/content/parser/token/underline.rs | 6 + static/public/assets/style.css | 1 + 20 files changed, 773 insertions(+), 121 deletions(-) diff --git a/.justfile b/.justfile index cd3f175..ad3be8c 100644 --- a/.justfile +++ b/.justfile @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 16a37ef..6b60a1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/graph.rs b/src/graph.rs index bcd4e11..e94c936 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -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)] diff --git a/src/log/level.rs b/src/log/level.rs index c5054c1..55c3627 100644 --- a/src/log/level.rs +++ b/src/log/level.rs @@ -59,28 +59,26 @@ impl From 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"); + } +} diff --git a/src/router/handlers/fixed.rs b/src/router/handlers/fixed.rs index 3d4d4be..67a764a 100644 --- a/src/router/handlers/fixed.rs +++ b/src/router/handlers/fixed.rs @@ -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); + } } diff --git a/src/router/handlers/graph.rs b/src/router/handlers/graph.rs index bdf3235..a1912c7 100644 --- a/src/router/handlers/graph.rs +++ b/src/router/handlers/graph.rs @@ -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); + } } diff --git a/src/router/handlers/mime.rs b/src/router/handlers/mime.rs index 4b82089..ee87ea0 100644 --- a/src/router/handlers/mime.rs +++ b/src/router/handlers/mime.rs @@ -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)); + } +} diff --git a/src/syntax/content.rs b/src/syntax/content.rs index f8ccefc..0cd700b 100644 --- a/src/syntax/content.rs +++ b/src/syntax/content.rs @@ -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, } +impl TokenOutput { + pub fn only(&self, kind: &Token) -> Vec { + let filter = |tokens: &[Token], k: &Token| -> Vec { + tokens + .iter() + .filter(|&t| discriminant(t) == discriminant(k)) + .cloned() + .collect::>() + }; + + 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::>() + } +} + 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); + } +} diff --git a/src/syntax/content/parser/token.rs b/src/syntax/content/parser/token.rs index 85ddee5..e8709fa 100644 --- a/src/syntax/content/parser/token.rs +++ b/src/syntax/content/parser/token.rs @@ -1,4 +1,4 @@ -use crate::syntax::content::Parseable as _; +use crate::syntax::content::Parseable; pub mod anchor; pub mod bold; diff --git a/src/syntax/content/parser/token/anchor.rs b/src/syntax/content/parser/token/anchor.rs index 94aac28..0eb2570 100644 --- a/src/syntax/content/parser/token/anchor.rs +++ b/src/syntax/content/parser/token/anchor.rs @@ -15,29 +15,6 @@ pub struct Anchor { } impl Anchor { - pub fn new( - text: &str, - destination: &str, - node: Option, - node_id: Option, - 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 -> "); + } } diff --git a/src/syntax/content/parser/token/bold.rs b/src/syntax/content/parser/token/bold.rs index e2a7efc..0252a67 100644 --- a/src/syntax/content/parser/token/bold.rs +++ b/src/syntax/content/parser/token/bold.rs @@ -76,4 +76,10 @@ mod tests { "Tk:Bold [closed]" ); } + + #[test] + fn flatten() { + let bold = Bold::new(false); + assert_eq!(bold.flatten(), ""); + } } diff --git a/src/syntax/content/parser/token/checkbox.rs b/src/syntax/content/parser/token/checkbox.rs index 656f419..777e499 100644 --- a/src/syntax/content/parser/token/checkbox.rs +++ b/src/syntax/content/parser/token/checkbox.rs @@ -75,4 +75,10 @@ mod tests { "Tk:CheckBox [empty]" ); } + + #[test] + fn flatten() { + let checkbox = CheckBox::new(false); + assert_eq!(checkbox.flatten(), ""); + } } diff --git a/src/syntax/content/parser/token/header.rs b/src/syntax/content/parser/token/header.rs index f238f73..e18603c 100644 --- a/src/syntax/content/parser/token/header.rs +++ b/src/syntax/content/parser/token/header.rs @@ -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}]") + ); + } } diff --git a/src/syntax/content/parser/token/item.rs b/src/syntax/content/parser/token/item.rs index 02b8431..7652898 100644 --- a/src/syntax/content/parser/token/item.rs +++ b/src/syntax/content/parser/token/item.rs @@ -89,4 +89,10 @@ mod tests { "Tk:Item [] dRMy4" ); } + + #[test] + fn flatten() { + let item = Item::new("", None); + assert_eq!(item.flatten(), ""); + } } diff --git a/src/syntax/content/parser/token/list.rs b/src/syntax/content/parser/token/list.rs index 721d226..ca73804 100644 --- a/src/syntax/content/parser/token/list.rs +++ b/src/syntax/content/parser/token/list.rs @@ -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
    \n\ +
  1. a
  2. \n\ +
  3. b
  4. \n\ +
  5. c
  6. \n\ +
\n\n" + ); + } } diff --git a/src/syntax/content/parser/token/oblique.rs b/src/syntax/content/parser/token/oblique.rs index 320626b..0d69468 100644 --- a/src/syntax/content/parser/token/oblique.rs +++ b/src/syntax/content/parser/token/oblique.rs @@ -79,4 +79,10 @@ mod tests { "Tk:Oblique [closed]" ); } + + #[test] + fn flatten() { + let oblique = Oblique::new(false); + assert_eq!(oblique.flatten(), ""); + } } diff --git a/src/syntax/content/parser/token/preformat.rs b/src/syntax/content/parser/token/preformat.rs index a55995b..89e71dc 100644 --- a/src/syntax/content/parser/token/preformat.rs +++ b/src/syntax/content/parser/token/preformat.rs @@ -99,4 +99,10 @@ mod tests { "Tk:PreFormat [unknown]" ); } + + #[test] + fn flatten() { + let preformat = PreFormat::new(false); + assert_eq!(preformat.flatten(), ""); + } } diff --git a/src/syntax/content/parser/token/strike.rs b/src/syntax/content/parser/token/strike.rs index c1a925a..14b4407 100644 --- a/src/syntax/content/parser/token/strike.rs +++ b/src/syntax/content/parser/token/strike.rs @@ -76,4 +76,10 @@ mod tests { "Tk:Strike [closed]" ); } + + #[test] + fn flatten() { + let strike = Strike::new(false); + assert_eq!(strike.flatten(), ""); + } } diff --git a/src/syntax/content/parser/token/underline.rs b/src/syntax/content/parser/token/underline.rs index a86e4b4..2da61b7 100644 --- a/src/syntax/content/parser/token/underline.rs +++ b/src/syntax/content/parser/token/underline.rs @@ -79,4 +79,10 @@ mod tests { "Tk:Underline [closed]" ); } + + #[test] + fn flatten() { + let underline = Underline::new(false); + assert_eq!(underline.flatten(), ""); + } } diff --git a/static/public/assets/style.css b/static/public/assets/style.css index 5859c52..1db505b 100644 --- a/static/public/assets/style.css +++ b/static/public/assets/style.css @@ -320,6 +320,7 @@ td, th { summary { margin-bottom: 15px; + cursor: pointer; } hr {