diff --git a/src/librustdoc/html/render/sorted_json.rs b/src/librustdoc/html/render/sorted_json.rs index 3a097733b8b2..e937382f5b0a 100644 --- a/src/librustdoc/html/render/sorted_json.rs +++ b/src/librustdoc/html/render/sorted_json.rs @@ -80,3 +80,6 @@ impl fmt::Display for EscapedJson { write!(f, "{}", json) } } + +#[cfg(test)] +mod tests; diff --git a/src/librustdoc/html/render/write_shared.rs b/src/librustdoc/html/render/write_shared.rs index eaebeadd8817..c2d2b4cd7d9f 100644 --- a/src/librustdoc/html/render/write_shared.rs +++ b/src/librustdoc/html/render/write_shared.rs @@ -15,7 +15,6 @@ use std::any::Any; use std::cell::RefCell; -use std::collections::hash_map::Entry; use std::ffi::OsString; use std::fs::File; use std::io::BufWriter; @@ -52,7 +51,7 @@ use crate::html::layout; use crate::html::render::search_index::build_index; use crate::html::render::search_index::SerializedSearchIndex; use crate::html::render::sorted_json::{EscapedJson, SortedJson}; -use crate::html::render::sorted_template::{self, SortedTemplate}; +use crate::html::render::sorted_template::{self, FileFormat, SortedTemplate}; use crate::html::render::{AssocItemLink, ImplRenderingParameters}; use crate::html::static_files::{self, suffix_path}; use crate::visit::DocVisitor; @@ -78,33 +77,29 @@ pub(crate) fn write_shared( let crate_name = krate.name(cx.tcx()); let crate_name = crate_name.as_str(); // rand let crate_name_json = SortedJson::serialize(crate_name); // "rand" - let external_crates = hack_get_external_crate_names(cx)?; + let external_crates = hack_get_external_crate_names(&cx.dst)?; let info = CrateInfo { src_files_js: SourcesPart::get(cx, &crate_name_json)?, - search_index_js: SearchIndexPart::get(cx, index)?, + search_index_js: SearchIndexPart::get(index, &cx.shared.resource_suffix)?, all_crates: AllCratesPart::get(crate_name_json.clone())?, crates_index: CratesIndexPart::get(&crate_name, &external_crates)?, trait_impl: TraitAliasPart::get(cx, &crate_name_json)?, type_impl: TypeAliasPart::get(cx, krate, &crate_name_json)?, }; - let crates_info = vec![info]; // we have info from just one crate + let crates = vec![info]; // we have info from just one crate. rest will found in out dir write_static_files(cx, &opt)?; let dst = &cx.dst; if opt.emit.is_empty() || opt.emit.contains(&EmitType::InvocationSpecific) { if cx.include_sources { - write_rendered_cci::(SourcesPart::blank, dst, &crates_info)?; + write_rendered_cci::(SourcesPart::blank, dst, &crates)?; } - write_rendered_cci::( - SearchIndexPart::blank, - dst, - &crates_info, - )?; - write_rendered_cci::(AllCratesPart::blank, dst, &crates_info)?; + write_rendered_cci::(SearchIndexPart::blank, dst, &crates)?; + write_rendered_cci::(AllCratesPart::blank, dst, &crates)?; } - write_rendered_cci::(TraitAliasPart::blank, dst, &crates_info)?; - write_rendered_cci::(TypeAliasPart::blank, dst, &crates_info)?; + write_rendered_cci::(TraitAliasPart::blank, dst, &crates)?; + write_rendered_cci::(TypeAliasPart::blank, dst, &crates)?; match &opt.index_page { Some(index_page) if opt.enable_index_page => { let mut md_opts = opt.clone(); @@ -119,7 +114,7 @@ pub(crate) fn write_shared( write_rendered_cci::( || CratesIndexPart::blank(cx), dst, - &crates_info, + &crates, )?; } _ => {} // they don't want an index page @@ -189,7 +184,8 @@ fn write_search_desc( let path = path.join(filename); let part = SortedJson::serialize(&part); let part = format!("searchState.loadedDescShard({encoded_crate_name}, {i}, {part})"); - write_create_parents(&path, part)?; + create_parents(&path)?; + try_err!(fs::write(&path, part), &path); } Ok(()) } @@ -286,8 +282,11 @@ else if (window.initSearch) window.initSearch(searchIndex);", ) } - fn get(cx: &Context<'_>, search_index: SortedJson) -> Result, Error> { - let path = suffix_path("search-index.js", &cx.shared.resource_suffix); + fn get( + search_index: SortedJson, + resource_suffix: &str, + ) -> Result, Error> { + let path = suffix_path("search-index.js", resource_suffix); let search_index = EscapedJson::from(search_index); Ok(PartsAndLocations::with(path, search_index)) } @@ -319,8 +318,8 @@ impl AllCratesPart { /// /// This is to match the current behavior of rustdoc, which allows you to get all crates /// on the index page, even if --enable-index-page is only passed to the last crate. -fn hack_get_external_crate_names(cx: &Context<'_>) -> Result, Error> { - let path = cx.dst.join("crates.js"); +fn hack_get_external_crate_names(doc_root: &Path) -> Result, Error> { + let path = doc_root.join("crates.js"); let Ok(content) = fs::read_to_string(&path) else { // they didn't emit invocation specific, so we just say there were no crates return Ok(Vec::default()); @@ -361,7 +360,7 @@ impl CratesIndexPart { match SortedTemplate::magic(&template, MAGIC) { Ok(template) => template, Err(e) => panic!( - "{e}: Object Replacement Character (U+FFFC) should not appear in the --index-page" + "Object Replacement Character (U+FFFC) should not appear in the --index-page: {e}" ), } } @@ -860,6 +859,21 @@ impl Serialize for AliasSerializableImpl { } } +fn get_path_parts( + dst: &Path, + crates_info: &[CrateInfo], +) -> FxHashMap> { + let mut templates: FxHashMap> = FxHashMap::default(); + crates_info.iter().map(|crate_info| crate_info.get::().parts.iter()).flatten().for_each( + |(path, part)| { + let path = dst.join(&path); + let part = part.to_string(); + templates.entry(path).or_default().push(part); + }, + ); + templates +} + /// Create all parents fn create_parents(path: &Path) -> Result<(), Error> { let parent = path.parent().expect("should not have an empty path here"); @@ -867,20 +881,13 @@ fn create_parents(path: &Path) -> Result<(), Error> { Ok(()) } -/// Create parents and then write -fn write_create_parents(path: &Path, content: String) -> Result<(), Error> { - create_parents(path)?; - try_err!(fs::write(path, content), path); - Ok(()) -} - /// Returns a blank template unless we could find one to append to -fn read_template_or_blank( +fn read_template_or_blank( mut make_blank: F, path: &Path, -) -> Result, Error> +) -> Result, Error> where - F: FnMut() -> SortedTemplate, + F: FnMut() -> SortedTemplate, { match fs::read_to_string(&path) { Ok(template) => Ok(try_err!(SortedTemplate::from_str(&template), &path)), @@ -898,27 +905,14 @@ fn write_rendered_cci( where F: FnMut() -> SortedTemplate, { - // read parts from disk - let path_parts = - crates_info.iter().map(|crate_info| crate_info.get::().parts.iter()).flatten(); - // read previous rendered cci from storage, append to them - let mut templates: FxHashMap> = Default::default(); - for (path, part) in path_parts { - let part = format!("{part}"); - let path = dst.join(&path); - match templates.entry(path.clone()) { - Entry::Vacant(entry) => { - let template = read_template_or_blank::<_, T>(&mut make_blank, &path)?; - let template = entry.insert(template); - template.append(part); - } - Entry::Occupied(mut t) => t.get_mut().append(part), - } - } - // write the merged cci to disk - for (path, template) in templates { + for (path, parts) in get_path_parts::(dst, crates_info) { create_parents(&path)?; + // read previous rendered cci from storage, append to them + let mut template = read_template_or_blank::<_, T::FileFormat>(&mut make_blank, &path)?; + for part in parts { + template.append(part); + } let file = try_err!(File::create(&path), &path); let mut file = BufWriter::new(file); try_err!(write!(file, "{template}"), &path); @@ -926,3 +920,6 @@ where } Ok(()) } + +#[cfg(test)] +mod tests; diff --git a/src/librustdoc/html/render/write_shared/tests.rs b/src/librustdoc/html/render/write_shared/tests.rs new file mode 100644 index 000000000000..000e233aec00 --- /dev/null +++ b/src/librustdoc/html/render/write_shared/tests.rs @@ -0,0 +1,206 @@ +use crate::html::render::sorted_json::{EscapedJson, SortedJson}; +use crate::html::render::sorted_template::{Html, SortedTemplate}; +use crate::html::render::write_shared::*; + +#[test] +fn hack_external_crate_names() { + let path = tempfile::TempDir::new().unwrap(); + let path = path.path(); + let crates = hack_get_external_crate_names(&path).unwrap(); + assert!(crates.is_empty()); + fs::write(path.join("crates.js"), r#"window.ALL_CRATES = ["a","b","c"];"#).unwrap(); + let crates = hack_get_external_crate_names(&path).unwrap(); + assert_eq!(crates, ["a".to_string(), "b".to_string(), "c".to_string()]); +} + +fn but_last_line(s: &str) -> &str { + let (before, _) = s.rsplit_once("\n").unwrap(); + before +} + +#[test] +fn sources_template() { + let mut template = SourcesPart::blank(); + assert_eq!( + but_last_line(&template.to_string()), + r"var srcIndex = new Map(JSON.parse('[]')); +createSrcSidebar();" + ); + template.append(EscapedJson::from(SortedJson::serialize("u")).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r#"var srcIndex = new Map(JSON.parse('["u"]')); +createSrcSidebar();"# + ); + template.append(EscapedJson::from(SortedJson::serialize("v")).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r#"var srcIndex = new Map(JSON.parse('["u","v"]')); +createSrcSidebar();"# + ); +} + +#[test] +fn sources_parts() { + let parts = SearchIndexPart::get(SortedJson::serialize(["foo", "bar"]), "suffix").unwrap(); + assert_eq!(&parts.parts[0].0, Path::new("search-indexsuffix.js")); + assert_eq!(&parts.parts[0].1.to_string(), r#"["foo","bar"]"#); +} + +#[test] +fn all_crates_template() { + let mut template = AllCratesPart::blank(); + assert_eq!(but_last_line(&template.to_string()), r"window.ALL_CRATES = [];"); + template.append(EscapedJson::from(SortedJson::serialize("b")).to_string()); + assert_eq!(but_last_line(&template.to_string()), r#"window.ALL_CRATES = ["b"];"#); + template.append(EscapedJson::from(SortedJson::serialize("a")).to_string()); + assert_eq!(but_last_line(&template.to_string()), r#"window.ALL_CRATES = ["a","b"];"#); +} + +#[test] +fn all_crates_parts() { + let parts = AllCratesPart::get(SortedJson::serialize("crate")).unwrap(); + assert_eq!(&parts.parts[0].0, Path::new("crates.js")); + assert_eq!(&parts.parts[0].1.to_string(), r#""crate""#); +} + +#[test] +fn search_index_template() { + let mut template = SearchIndexPart::blank(); + assert_eq!( + but_last_line(&template.to_string()), + r"var searchIndex = new Map(JSON.parse('[]')); +if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; +else if (window.initSearch) window.initSearch(searchIndex);" + ); + template.append(EscapedJson::from(SortedJson::serialize([1, 2])).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r"var searchIndex = new Map(JSON.parse('[[1,2]]')); +if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; +else if (window.initSearch) window.initSearch(searchIndex);" + ); + template.append(EscapedJson::from(SortedJson::serialize([4, 3])).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r"var searchIndex = new Map(JSON.parse('[[1,2],[4,3]]')); +if (typeof exports !== 'undefined') exports.searchIndex = searchIndex; +else if (window.initSearch) window.initSearch(searchIndex);" + ); +} + +#[test] +fn crates_index_part() { + let external_crates = ["bar".to_string(), "baz".to_string()]; + let mut parts = CratesIndexPart::get("foo", &external_crates).unwrap(); + parts.parts.sort_by(|a, b| a.1.to_string().cmp(&b.1.to_string())); + + assert_eq!(&parts.parts[0].0, Path::new("index.html")); + assert_eq!(&parts.parts[0].1.to_string(), r#"
  • bar
  • "#); + + assert_eq!(&parts.parts[1].0, Path::new("index.html")); + assert_eq!(&parts.parts[1].1.to_string(), r#"
  • baz
  • "#); + + assert_eq!(&parts.parts[2].0, Path::new("index.html")); + assert_eq!(&parts.parts[2].1.to_string(), r#"
  • foo
  • "#); +} + +#[test] +fn trait_alias_template() { + let mut template = TraitAliasPart::blank(); + assert_eq!( + but_last_line(&template.to_string()), + r#"(function() { + var implementors = Object.fromEntries([]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})()"#, + ); + template.append(SortedJson::serialize(["a"]).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r#"(function() { + var implementors = Object.fromEntries([["a"]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})()"#, + ); + template.append(SortedJson::serialize(["b"]).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r#"(function() { + var implementors = Object.fromEntries([["a"],["b"]]); + if (window.register_implementors) { + window.register_implementors(implementors); + } else { + window.pending_implementors = implementors; + } +})()"#, + ); +} + +#[test] +fn type_alias_template() { + let mut template = TypeAliasPart::blank(); + assert_eq!( + but_last_line(&template.to_string()), + r#"(function() { + var type_impls = Object.fromEntries([]); + if (window.register_type_impls) { + window.register_type_impls(type_impls); + } else { + window.pending_type_impls = type_impls; + } +})()"#, + ); + template.append(SortedJson::serialize(["a"]).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r#"(function() { + var type_impls = Object.fromEntries([["a"]]); + if (window.register_type_impls) { + window.register_type_impls(type_impls); + } else { + window.pending_type_impls = type_impls; + } +})()"#, + ); + template.append(SortedJson::serialize(["b"]).to_string()); + assert_eq!( + but_last_line(&template.to_string()), + r#"(function() { + var type_impls = Object.fromEntries([["a"],["b"]]); + if (window.register_type_impls) { + window.register_type_impls(type_impls); + } else { + window.pending_type_impls = type_impls; + } +})()"#, + ); +} + +#[test] +fn read_template_test() { + let path = tempfile::TempDir::new().unwrap(); + let path = path.path().join("file.html"); + let make_blank = || SortedTemplate::::before_after("
    ", "
    "); + + let template = read_template_or_blank(make_blank, &path).unwrap(); + assert_eq!(but_last_line(&template.to_string()), "
    "); + fs::write(&path, template.to_string()).unwrap(); + let mut template = read_template_or_blank(make_blank, &path).unwrap(); + template.append("".to_string()); + fs::write(&path, template.to_string()).unwrap(); + let mut template = read_template_or_blank(make_blank, &path).unwrap(); + template.append("
    ".to_string()); + fs::write(&path, template.to_string()).unwrap(); + let template = read_template_or_blank(make_blank, &path).unwrap(); + + assert_eq!(but_last_line(&template.to_string()), "

    "); +}