diff --git a/src/defs.rs b/src/defs.rs index bc2a10982..8277b3169 100644 --- a/src/defs.rs +++ b/src/defs.rs @@ -92,7 +92,7 @@ pub struct HtmlItem { pub result: CovResult, } -#[derive(Clone, Debug, Default, serde::Serialize)] +#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] pub struct HtmlStats { pub total_lines: usize, pub covered_lines: usize, @@ -102,26 +102,80 @@ pub struct HtmlStats { pub covered_branches: usize, } -#[derive(Clone, Debug, serde::Serialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct HtmlFileStats { pub stats: HtmlStats, pub abs_prefix: Option, } -#[derive(Clone, Debug, serde::Serialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct HtmlDirStats { pub files: BTreeMap, pub stats: HtmlStats, pub abs_prefix: Option, } -#[derive(Debug, Default, serde::Serialize)] +#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] pub struct HtmlGlobalStats { pub dirs: BTreeMap, pub stats: HtmlStats, pub abs_prefix: Option, } +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum HtmlItemStats { + Directory(HtmlDirStats), + File(HtmlFileStats), +} + +impl HtmlGlobalStats { + pub fn list(&self, dir: &str) -> BTreeMap { + let mut result = BTreeMap::new(); + + // Add files from the specified directory + if let Some(dir_stats) = self.dirs.get(dir) { + for (file_name, file_stats) in &dir_stats.files { + result.insert(file_name.clone(), HtmlItemStats::File(file_stats.clone())); + } + } + + // Add subdirectories as entries + if dir.is_empty() { + // For root directory, add top-level directories + for (dir_path, dir_stats) in &self.dirs { + if !dir_path.is_empty() && !dir_path.contains('/') { + result.insert( + dir_path.clone(), + HtmlItemStats::Directory(dir_stats.clone()), + ); + } + } + } else { + // For specific directory, add immediate subdirectories + let prefix = if dir.ends_with('/') { + dir.to_string() + } else { + format!("{}/", dir) + }; + + for (dir_path, dir_stats) in &self.dirs { + if dir_path.starts_with(&prefix) { + let suffix = &dir_path[prefix.len()..]; + if !suffix.is_empty() && !suffix.contains('/') { + result.insert( + suffix.to_string(), + HtmlItemStats::Directory(dir_stats.clone()), + ); + } + } + } + } + + result + } +} + pub type HtmlJobReceiver = Receiver>; pub type HtmlJobSender = Sender>; @@ -155,3 +209,80 @@ pub struct JacocoReport { pub lines: BTreeMap, pub branches: BTreeMap>, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_html_global_stats_list() { + let global_json = serde_json::json!({ + "dirs": { + "": { + "files": { + "build.rs": { + "stats": {"total_lines": 30, "covered_lines": 25, "total_funs": 3, "covered_funs": 2, "total_branches": 6, "covered_branches": 5}, + "abs_prefix": null + } + }, + "stats": {"total_lines": 30, "covered_lines": 25, "total_funs": 3, "covered_funs": 2, "total_branches": 6, "covered_branches": 5}, + "abs_prefix": null + }, + "src": { + "files": { + "lib.rs": { + "stats": {"total_lines": 50, "covered_lines": 40, "total_funs": 5, "covered_funs": 4, "total_branches": 10, "covered_branches": 8}, + "abs_prefix": null + } + }, + "stats": {"total_lines": 100, "covered_lines": 80, "total_funs": 10, "covered_funs": 8, "total_branches": 20, "covered_branches": 16}, + "abs_prefix": null + }, + "src/utils": { + "files": { + "mod.rs": { + "stats": {"total_lines": 50, "covered_lines": 40, "total_funs": 5, "covered_funs": 4, "total_branches": 10, "covered_branches": 8}, + "abs_prefix": null + } + }, + "stats": {"total_lines": 50, "covered_lines": 40, "total_funs": 5, "covered_funs": 4, "total_branches": 10, "covered_branches": 8}, + "abs_prefix": null + } + }, + "stats": {"total_lines": 130, "covered_lines": 105, "total_funs": 13, "covered_funs": 10, "total_branches": 26, "covered_branches": 21}, + "abs_prefix": null + }); + + let global: HtmlGlobalStats = serde_json::from_value(global_json).unwrap(); + + let root_items = global.list(""); + assert_eq!(root_items.len(), 2); + assert!(root_items.contains_key("build.rs")); + assert!(root_items.contains_key("src")); + + // Check that build.rs is a file and src is a directory + match root_items.get("build.rs").unwrap() { + HtmlItemStats::File(_) => {} + HtmlItemStats::Directory(_) => panic!("build.rs should be a file"), + } + match root_items.get("src").unwrap() { + HtmlItemStats::Directory(_) => {} + HtmlItemStats::File(_) => panic!("src should be a directory"), + } + + let src_items = global.list("src"); + assert_eq!(src_items.len(), 2); + assert!(src_items.contains_key("lib.rs")); + assert!(src_items.contains_key("utils")); + + // Check that utils is a directory + match src_items.get("utils").unwrap() { + HtmlItemStats::Directory(_) => {} + HtmlItemStats::File(_) => panic!("utils should be a directory"), + } + + let utils_items = global.list("src/utils"); + assert_eq!(utils_items.len(), 1); + assert!(utils_items.contains_key("mod.rs")); + } +} diff --git a/src/html.rs b/src/html.rs index 0c9cfad31..1d659d1dc 100644 --- a/src/html.rs +++ b/src/html.rs @@ -334,11 +334,11 @@ pub fn gen_index(tera: &Tera, global: &HtmlGlobalStats, conf: &Config, output: & let mut ctx = make_context(conf, "."); let empty: &[&str] = &[]; + let items = global.list(""); ctx.insert("current", "top_level"); ctx.insert("parents", empty); ctx.insert("stats", &global.stats); - ctx.insert("items", &global.dirs); - ctx.insert("kind", "Directory"); + ctx.insert("items", &items); let out = tera.render("index.html", &ctx).unwrap(); @@ -347,14 +347,22 @@ pub fn gen_index(tera: &Tera, global: &HtmlGlobalStats, conf: &Config, output: & return; } - for (dir_name, dir_stats) in global.dirs.iter() { - gen_dir_index(tera, dir_name, dir_stats, conf, output); + for (dir_name, item_type) in items.iter() { + match item_type { + HtmlItemStats::Directory(dir_stats) => { + gen_dir_index(tera, dir_name, global, dir_stats, conf, output); + } + HtmlItemStats::File(_) => { + // Files don't generate directory indexes, skip + } + } } } pub fn gen_dir_index( tera: &Tera, dir_name: &str, + global: &HtmlGlobalStats, dir_stats: &HtmlDirStats, conf: &Config, output: &Path, @@ -364,7 +372,7 @@ pub fn gen_dir_index( let prefix = "../".repeat(layers) + "index.html"; let output_file = output.join(index); create_parent(&output_file); - let mut output = match File::create(&output_file) { + let mut output_file = match File::create(&output_file) { Err(_) => { eprintln!("Cannot create file {output_file:?}"); return; @@ -380,14 +388,31 @@ pub fn gen_dir_index( }; ctx.insert("parents", &[(parent_link, "top_level")]); ctx.insert("stats", &dir_stats.stats); - ctx.insert("items", &dir_stats.files); - ctx.insert("kind", "File"); + let items = global.list(dir_name); + ctx.insert("items", &items); let out = tera.render("index.html", &ctx).unwrap(); - if output.write_all(out.as_bytes()).is_err() { + if output_file.write_all(out.as_bytes()).is_err() { eprintln!("Cannot write the file {output_file:?}"); } + + let parent = if dir_name.is_empty() { + dir_name.to_string() + } else { + dir_name.to_string() + "/" + }; + for (dir_name, item_type) in items.iter() { + match item_type { + HtmlItemStats::Directory(dir_stats) => { + let full_dir_name = parent.clone() + dir_name; + gen_dir_index(tera, &full_dir_name, global, dir_stats, conf, output); + } + HtmlItemStats::File(_) => { + // Files don't generate directory indexes, skip + } + } + } } fn gen_html( diff --git a/src/templates/index.html b/src/templates/index.html index fabd01499..efb14459a 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -8,7 +8,7 @@ - + {% if branch_enabled %} @@ -17,23 +17,23 @@ - {%- if kind == "Directory" -%} - {%- for item, info in items -%} - {% if info.abs_prefix and info.abs_prefix != "" %} - {{ macros::stats_line(name=item, url=info.abs_prefix~item~"/index.html", stats=info.stats, precision=precision) }} - {% else %} - {{ macros::stats_line(name=item, url=item~"/index.html", stats=info.stats, precision=precision) }} - {% endif %} - {%- endfor -%} - {%- else -%} - {%- for item, info in items -%} - {% if info.abs_prefix and info.abs_prefix != "" %} - {{ macros::stats_line(name=item, url=info.abs_prefix~"/"~item~".html", stats=info.stats, precision=precision) }} - {% else %} - {{ macros::stats_line(name=item, url=item~".html", stats=info.stats, precision=precision) }} - {% endif %} - {%- endfor -%} - {%- endif -%} + {%- for item, info in items -%} + {%- if info.files is defined -%} + {# This is a directory #} + {% if info.abs_prefix and info.abs_prefix != "" %} + {{ macros::stats_line(name=item, url=info.abs_prefix~item~"/index.html", stats=info.stats, precision=precision) }} + {% else %} + {{ macros::stats_line(name=item, url=item~"/index.html", stats=info.stats, precision=precision) }} + {% endif %} + {%- else -%} + {# This is a file #} + {% if info.abs_prefix and info.abs_prefix != "" %} + {{ macros::stats_line(name=item, url=info.abs_prefix~"/"~item~".html", stats=info.stats, precision=precision) }} + {% else %} + {{ macros::stats_line(name=item, url=item~".html", stats=info.stats, precision=precision) }} + {% endif %} + {%- endif -%} + {%- endfor -%}
{{ kind }}Name Line Coverage Functions
{%- endblock content -%}