1pub mod dependencies;
25pub mod genindex;
26pub mod metadata;
27pub mod numbers;
28pub mod py_domain;
29pub mod resolve;
30pub mod std_domain;
31pub mod toctree;
32
33use std::collections::{BTreeMap, BTreeSet};
34use std::path::{Path, PathBuf};
35
36use serde::{Deserialize, Serialize};
37use serde_json::{json, Map as JsonMap, Value as JsonValue};
38
39use crate::doctree::Node;
40
41pub const ENV_VERSION: u32 = 3;
74
75const ENV_FILENAME: &str = "env.bin";
77
78pub use py_domain::PyDomainData;
79pub use std_domain::StdDomainData;
80
81#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
89pub struct IndexEntryRecord {
90 pub entry_type: String,
91 pub value: String,
92 pub target_id: String,
93 pub main: bool,
94 pub category_key: Option<String>,
95}
96
97pub struct FileTimes<'a> {
107 pub source_modified_us: &'a dyn Fn(&str) -> Option<u64>,
109 pub doctree_exists: &'a dyn Fn(&str) -> bool,
114 pub dependency_modified_us: &'a dyn Fn(&Path) -> Option<u64>,
116}
117
118#[derive(Debug, Clone, Default, PartialEq, Eq)]
125pub struct Outdated {
126 pub added: BTreeSet<String>,
128 pub changed: BTreeSet<String>,
130 pub removed: BTreeSet<String>,
133}
134
135impl Outdated {
136 pub fn to_read(&self) -> BTreeSet<String> {
138 self.added.union(&self.changed).cloned().collect()
139 }
140}
141
142#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
146pub struct BuildEnvironment {
147 pub version: u32,
151 pub root_doc: String,
157 pub all_docs: BTreeMap<String, u64>,
159 pub dependencies: BTreeMap<String, BTreeSet<PathBuf>>,
162 pub included: BTreeMap<String, BTreeSet<String>>,
164 pub reread_always: BTreeSet<String>,
166 pub metadata: BTreeMap<String, BTreeMap<String, String>>,
169 pub titles: BTreeMap<String, Node>,
170 pub longtitles: BTreeMap<String, Node>,
171 pub tocs: BTreeMap<String, Node>,
174 pub toc_num_entries: BTreeMap<String, u32>,
175 pub toc_secnumbers: BTreeMap<String, BTreeMap<String, Vec<u32>>>,
178 pub toc_fignumbers: BTreeMap<String, BTreeMap<String, BTreeMap<String, Vec<u32>>>>,
180 pub toctree_includes: BTreeMap<String, Vec<String>>,
182 pub files_to_rebuild: BTreeMap<String, BTreeSet<String>>,
186 pub glob_toctrees: BTreeSet<String>,
187 pub numbered_toctrees: BTreeSet<String>,
188 pub std: StdDomainData,
189 pub py: PyDomainData,
192 pub index_entries: BTreeMap<String, Vec<IndexEntryRecord>>,
194}
195
196impl BuildEnvironment {
197 pub fn load(cache_dir: &Path) -> Option<Self> {
204 let bytes = std::fs::read(cache_dir.join(ENV_FILENAME)).ok()?;
205 let (env, _consumed): (Self, usize) =
206 bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).ok()?;
207 if env.version != ENV_VERSION {
208 return None;
209 }
210 Some(env)
211 }
212
213 pub fn save(&mut self, cache_dir: &Path) -> anyhow::Result<()> {
222 let previous = std::mem::replace(&mut self.version, ENV_VERSION);
223 let write = || -> anyhow::Result<()> {
224 std::fs::create_dir_all(cache_dir)?;
225 let bytes = bincode::serde::encode_to_vec(&*self, bincode::config::standard())?;
226 std::fs::write(cache_dir.join(ENV_FILENAME), bytes)?;
227 Ok(())
228 };
229 match write() {
230 Ok(()) => Ok(()),
231 Err(e) => {
232 self.version = previous;
233 Err(e)
234 }
235 }
236 }
237
238 pub fn get_outdated_files(
252 &self,
253 found: &BTreeSet<String>,
254 config_changed: bool,
255 times: &FileTimes<'_>,
256 ) -> Outdated {
257 let mut outdated = Outdated {
258 removed: self
259 .all_docs
260 .keys()
261 .filter(|docname| !found.contains(docname.as_str()))
262 .cloned()
263 .collect(),
264 ..Default::default()
265 };
266
267 if config_changed {
268 outdated.added = found.clone();
271 return outdated;
272 }
273
274 for docname in found {
275 if !self.all_docs.contains_key(docname) {
276 outdated.added.insert(docname.clone());
277 } else if self.has_doc_changed(docname, times) {
278 outdated.changed.insert(docname.clone());
279 }
280 }
281
282 if !outdated.added.is_empty() || !outdated.removed.is_empty() {
283 for docname in &self.glob_toctrees {
284 if found.contains(docname) && !outdated.added.contains(docname) {
285 outdated.changed.insert(docname.clone());
286 }
287 }
288 }
289
290 outdated
291 }
292
293 fn has_doc_changed(&self, docname: &str, times: &FileTimes<'_>) -> bool {
297 if self.reread_always.contains(docname) {
298 return true;
299 }
300 if !(times.doctree_exists)(docname) {
301 return true;
302 }
303 let Some(&read_time) = self.all_docs.get(docname) else {
304 return true;
305 };
306 match (times.source_modified_us)(docname) {
307 None => return true,
308 Some(modified) if modified > read_time => return true,
309 Some(_) => {}
310 }
311 for dependency in self.dependencies.get(docname).into_iter().flatten() {
315 match (times.dependency_modified_us)(dependency) {
316 None => return true,
317 Some(modified) if modified > read_time => return true,
318 Some(_) => {}
319 }
320 }
321 false
322 }
323
324 pub fn clear_doc(&mut self, docname: &str) {
337 self.all_docs.remove(docname);
338 self.included.remove(docname);
339 self.reread_always.remove(docname);
340 self.dependencies.remove(docname);
341 self.metadata.remove(docname);
342
343 self.titles.remove(docname);
344 self.longtitles.remove(docname);
345
346 self.tocs.remove(docname);
347 self.toc_secnumbers.remove(docname);
348 self.toc_fignumbers.remove(docname);
349 self.toc_num_entries.remove(docname);
350 self.toctree_includes.remove(docname);
351 self.glob_toctrees.remove(docname);
352 self.numbered_toctrees.remove(docname);
353
354 self.files_to_rebuild.retain(|_, containing| {
357 containing.remove(docname);
358 !containing.is_empty()
359 });
360
361 self.std
362 .progoptions
363 .retain(|_, (fn_, _)| fn_.as_str() != docname);
364 self.std
365 .objects
366 .retain(|_, (fn_, _)| fn_.as_str() != docname);
367 self.std.terms.retain(|_, (fn_, _)| fn_.as_str() != docname);
368 self.std
369 .labels
370 .retain(|_, (fn_, _, _)| fn_.as_str() != docname);
371 self.std
372 .anonlabels
373 .retain(|_, (fn_, _)| fn_.as_str() != docname);
374
375 self.py.clear_doc(docname);
378
379 self.index_entries.remove(docname);
380 }
381
382 pub fn snapshot(&self) -> JsonValue {
391 let objects: Vec<JsonValue> = self
392 .std
393 .objects
394 .iter()
395 .map(|((objtype, name), (docname, labelid))| {
396 json!({
397 "objtype": objtype,
398 "name": name,
399 "docname": docname,
400 "labelid": labelid,
401 })
402 })
403 .collect();
404
405 let progoptions: Vec<JsonValue> = self
406 .std
407 .progoptions
408 .iter()
409 .map(|((program, name), (docname, labelid))| {
410 json!({
411 "program": program,
412 "name": name,
413 "docname": docname,
414 "labelid": labelid,
415 })
416 })
417 .collect();
418
419 let py_objects: Vec<JsonValue> = self
425 .py
426 .objects
427 .iter()
428 .map(|(name, entry)| {
429 json!({
430 "name": name,
431 "docname": entry.docname,
432 "node_id": entry.node_id,
433 "objtype": entry.objtype,
434 "aliased": entry.aliased,
435 })
436 })
437 .collect();
438
439 let py_modules: Vec<JsonValue> = self
440 .py
441 .modules
442 .iter()
443 .map(|(name, entry)| {
444 json!({
445 "name": name,
446 "docname": entry.docname,
447 "node_id": entry.node_id,
448 "synopsis": entry.synopsis,
449 "platform": entry.platform,
450 "deprecated": entry.deprecated,
451 })
452 })
453 .collect();
454
455 let mut index_entries = JsonMap::new();
456 for (docname, entries) in &self.index_entries {
457 let arr: Vec<JsonValue> = entries
458 .iter()
459 .map(|e| {
460 json!([
461 e.entry_type,
462 e.value,
463 e.target_id,
464 if e.main { "main" } else { "" },
465 e.category_key,
466 ])
467 })
468 .collect();
469 index_entries.insert(docname.clone(), JsonValue::Array(arr));
470 }
471
472 let relations: JsonMap<String, JsonValue> = toctree::collect_relations(self)
476 .into_iter()
477 .map(|(docname, (parent, prev, next))| (docname, json!([parent, prev, next])))
478 .collect();
479
480 json!({
481 "version": self.version,
482 "root_doc": self.root_doc,
483 "all_docs": self.all_docs,
484 "relations": JsonValue::Object(relations),
485 "metadata": self.metadata,
486 "dependencies": self.dependencies,
487 "included": self.included,
488 "reread_always": self.reread_always,
489 "titles_pformat": pformat_map(&self.titles),
490 "longtitles_pformat": pformat_map(&self.longtitles),
491 "tocs_pformat": pformat_map(&self.tocs),
492 "toc_num_entries": self.toc_num_entries,
493 "toc_secnumbers": self.toc_secnumbers,
494 "toc_fignumbers": self.toc_fignumbers,
495 "toctree_includes": self.toctree_includes,
496 "files_to_rebuild": self.files_to_rebuild,
497 "glob_toctrees": self.glob_toctrees,
498 "numbered_toctrees": self.numbered_toctrees,
499 "std": {
500 "labels": self.std.labels,
501 "anonlabels": self.std.anonlabels,
502 "objects": objects,
503 "progoptions": progoptions,
504 "terms": self.std.terms,
505 },
506 "py_objects": py_objects,
507 "py_modules": py_modules,
508 "index_entries": JsonValue::Object(index_entries),
509 })
510 }
511}
512
513fn pformat_map(nodes: &BTreeMap<String, Node>) -> JsonValue {
516 let map: JsonMap<String, JsonValue> = nodes
517 .iter()
518 .map(|(docname, node)| (docname.clone(), JsonValue::String(node.pformat())))
519 .collect();
520 JsonValue::Object(map)
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use crate::doctree::{kinds, Span};
527
528 fn sample_node() -> Node {
529 let mut root = Node::elem(kinds::BULLET_LIST, Span::ZERO);
530 let mut item = Node::elem(kinds::LIST_ITEM, Span::ZERO);
531 item.children
532 .push(Node::text_node("Chapter One", Span::ZERO));
533 root.children.push(item);
534 root
535 }
536
537 fn populated_env() -> BuildEnvironment {
538 let mut env = BuildEnvironment {
539 version: ENV_VERSION,
540 root_doc: "index".to_string(),
541 ..Default::default()
542 };
543 env.all_docs.insert("index".to_string(), 1_700_000_000);
544 env.metadata.insert(
545 "index".to_string(),
546 BTreeMap::from([("orphan".to_string(), String::new())]),
547 );
548 env.dependencies.insert(
549 "index".to_string(),
550 BTreeSet::from([PathBuf::from("/src/index.rst")]),
551 );
552 env.included.insert(
553 "index".to_string(),
554 BTreeSet::from(["chapters/intro".to_string()]),
555 );
556 env.reread_always.insert("index".to_string());
557 env.titles.insert("index".to_string(), sample_node());
558 env.longtitles.insert("index".to_string(), sample_node());
559 env.tocs.insert("index".to_string(), sample_node());
560 env.toc_num_entries.insert("index".to_string(), 3);
561 env.toc_secnumbers.insert(
562 "index".to_string(),
563 BTreeMap::from([(String::new(), vec![1]), ("#sec".to_string(), vec![1, 1])]),
564 );
565 env.toc_fignumbers.insert(
566 "index".to_string(),
567 BTreeMap::from([(
568 "figure".to_string(),
569 BTreeMap::from([("fig1".to_string(), vec![1])]),
570 )]),
571 );
572 env.toctree_includes.insert(
573 "index".to_string(),
574 vec!["chapters/intro".to_string(), "chapters/two".to_string()],
575 );
576 env.files_to_rebuild.insert(
577 "chapters/intro".to_string(),
578 BTreeSet::from(["index".to_string()]),
579 );
580 env.glob_toctrees.insert("index".to_string());
581 env.numbered_toctrees.insert("index".to_string());
582 env.std.labels.insert(
586 "intro".to_string(),
587 (
588 "index".to_string(),
589 "intro-id".to_string(),
590 "Introduction".to_string(),
591 ),
592 );
593 env.std.anonlabels.insert(
594 "intro".to_string(),
595 ("index".to_string(), "intro-id".to_string()),
596 );
597 env.std.objects.insert(
598 ("envvar".to_string(), "PATH".to_string()),
599 ("index".to_string(), "envvar-path".to_string()),
600 );
601 env.std.progoptions.insert(
602 (Some("myprog".to_string()), "--verbose".to_string()),
603 ("index".to_string(), "cmdoption-verbose".to_string()),
604 );
605 env.std.terms.insert(
606 "glossary term".to_string(),
607 ("index".to_string(), "term-glossary-term".to_string()),
608 );
609 env.py.note_object(
612 "zeta.func",
613 py_domain::PyObjectEntry {
614 docname: "index".to_string(),
615 node_id: "zeta.func".to_string(),
616 objtype: "function".to_string(),
617 aliased: false,
618 },
619 );
620 env.py.note_object(
621 "alpha.func",
622 py_domain::PyObjectEntry {
623 docname: "index".to_string(),
624 node_id: "alpha.func".to_string(),
625 objtype: "function".to_string(),
626 aliased: true,
627 },
628 );
629 env.py.note_module(
630 "zeta",
631 py_domain::PyModuleEntry {
632 docname: "index".to_string(),
633 node_id: "module-zeta".to_string(),
634 synopsis: "Zed things.".to_string(),
635 platform: "posix".to_string(),
636 deprecated: true,
637 },
638 );
639 env.index_entries.insert(
640 "index".to_string(),
641 vec![IndexEntryRecord {
642 entry_type: "single".to_string(),
643 value: "PATH".to_string(),
644 target_id: "index-0".to_string(),
645 main: true,
646 category_key: None,
647 }],
648 );
649 env
650 }
651
652 #[test]
653 fn round_trip_through_bincode_preserves_node_valued_fields() {
654 let tmp = tempfile::TempDir::new().unwrap();
655 let mut env = populated_env();
656
657 env.save(tmp.path()).expect("save succeeds");
658 let restored = BuildEnvironment::load(tmp.path()).expect("load succeeds");
659
660 assert_eq!(restored, env);
661 assert_eq!(restored.titles["index"], sample_node());
664 assert_eq!(restored.tocs["index"].pformat(), sample_node().pformat());
665 }
666
667 #[test]
668 fn save_always_stamps_current_env_version() {
669 let tmp = tempfile::TempDir::new().unwrap();
670 let mut env = BuildEnvironment {
671 version: 0, ..Default::default()
673 };
674
675 env.save(tmp.path()).unwrap();
676
677 assert_eq!(env.version, ENV_VERSION);
678 let restored = BuildEnvironment::load(tmp.path()).unwrap();
679 assert_eq!(restored.version, ENV_VERSION);
680 }
681
682 #[test]
683 fn failed_save_leaves_the_in_memory_version_untouched() {
684 let tmp = tempfile::TempDir::new().unwrap();
688 let blocked = tmp.path().join("not-a-dir");
689 std::fs::write(&blocked, b"").unwrap();
690
691 let mut env = BuildEnvironment {
692 version: 0,
693 ..Default::default()
694 };
695 assert!(env.save(&blocked).is_err());
696 assert_eq!(env.version, 0);
697 }
698
699 #[test]
700 fn load_returns_none_when_file_is_missing() {
701 let tmp = tempfile::TempDir::new().unwrap();
702 assert!(BuildEnvironment::load(tmp.path()).is_none());
703 }
704
705 #[test]
706 fn load_returns_none_on_decode_error() {
707 let tmp = tempfile::TempDir::new().unwrap();
708 std::fs::write(
709 tmp.path().join(ENV_FILENAME),
710 b"not a valid bincode blob at all",
711 )
712 .unwrap();
713 assert!(BuildEnvironment::load(tmp.path()).is_none());
714 }
715
716 #[test]
717 fn load_returns_none_when_version_does_not_match_current() {
718 let tmp = tempfile::TempDir::new().unwrap();
719 let stale = BuildEnvironment {
720 version: ENV_VERSION + 1,
721 ..Default::default()
722 };
723 let bytes = bincode::serde::encode_to_vec(&stale, bincode::config::standard()).unwrap();
724 std::fs::write(tmp.path().join(ENV_FILENAME), bytes).unwrap();
725
726 assert!(BuildEnvironment::load(tmp.path()).is_none());
727 }
728
729 #[test]
730 fn clear_doc_scrubs_every_per_doc_field() {
731 let mut env = populated_env();
732 env.files_to_rebuild
736 .get_mut("chapters/intro")
737 .unwrap()
738 .insert("other".to_string());
739 env.all_docs.insert("other".to_string(), 1_700_000_001);
740
741 env.clear_doc("index");
742
743 assert!(!env.all_docs.contains_key("index"));
744 assert!(!env.included.contains_key("index"));
745 assert!(!env.reread_always.contains("index"));
746 assert!(!env.dependencies.contains_key("index"));
747 assert!(!env.metadata.contains_key("index"));
748 assert!(!env.titles.contains_key("index"));
749 assert!(!env.longtitles.contains_key("index"));
750 assert!(!env.tocs.contains_key("index"));
751 assert!(!env.toc_secnumbers.contains_key("index"));
752 assert!(!env.toc_fignumbers.contains_key("index"));
753 assert!(!env.toc_num_entries.contains_key("index"));
754 assert!(!env.toctree_includes.contains_key("index"));
755 assert!(!env.glob_toctrees.contains("index"));
756 assert!(!env.numbered_toctrees.contains("index"));
757
758 assert_eq!(
761 env.files_to_rebuild.get("chapters/intro"),
762 Some(&BTreeSet::from(["other".to_string()]))
763 );
764
765 assert!(!env.std.labels.contains_key("intro"));
772 assert!(!env.std.anonlabels.contains_key("intro"));
773 assert_eq!(env.std.labels, StdDomainData::default().labels);
774 assert_eq!(env.std.anonlabels, StdDomainData::default().anonlabels);
775 assert!(env.std.objects.is_empty());
776 assert!(env.std.progoptions.is_empty());
777 assert!(env.std.terms.is_empty());
778 assert!(env.py.objects.is_empty() && env.py.objects_index.is_empty());
779 assert!(env.py.modules.is_empty() && env.py.modules_index.is_empty());
780 assert!(env.index_entries.is_empty());
781 }
782
783 #[test]
784 fn clear_doc_deletes_files_to_rebuild_key_when_value_set_becomes_empty() {
785 let mut env = BuildEnvironment::default();
786 env.files_to_rebuild.insert(
787 "chapters/intro".to_string(),
788 BTreeSet::from(["index".to_string()]),
789 );
790
791 env.clear_doc("index");
792
793 assert!(
794 !env.files_to_rebuild.contains_key("chapters/intro"),
795 "an emptied value-set must delete its key, not linger as an empty set"
796 );
797 }
798
799 #[derive(Default)]
803 struct Fs {
804 sources: BTreeMap<String, Option<u64>>,
805 doctrees: BTreeSet<String>,
806 deps: BTreeMap<PathBuf, Option<u64>>,
807 }
808
809 const READ_TIME: u64 = 1_000_000;
810
811 fn steady_state() -> (BuildEnvironment, Fs) {
813 let mut env = BuildEnvironment {
814 root_doc: "index".to_string(),
815 ..Default::default()
816 };
817 env.all_docs.insert("index".to_string(), READ_TIME);
818 env.all_docs.insert("a".to_string(), READ_TIME);
819 let fs = Fs {
820 sources: BTreeMap::from([
821 ("index".to_string(), Some(READ_TIME - 1)),
822 ("a".to_string(), Some(READ_TIME - 1)),
823 ]),
824 doctrees: BTreeSet::from(["index".to_string(), "a".to_string()]),
825 deps: BTreeMap::new(),
826 };
827 (env, fs)
828 }
829
830 fn found(docnames: &[&str]) -> BTreeSet<String> {
831 docnames.iter().map(|d| d.to_string()).collect()
832 }
833
834 fn outdated_with(
835 env: &BuildEnvironment,
836 fs: &Fs,
837 docnames: &[&str],
838 config_changed: bool,
839 ) -> Outdated {
840 env.get_outdated_files(
841 &found(docnames),
842 config_changed,
843 &FileTimes {
844 source_modified_us: &|docname| fs.sources.get(docname).copied().flatten(),
845 doctree_exists: &|docname| fs.doctrees.contains(docname),
846 dependency_modified_us: &|path| fs.deps.get(path).copied().flatten(),
847 },
848 )
849 }
850
851 fn outdated(env: &BuildEnvironment, fs: &Fs, docnames: &[&str]) -> Outdated {
852 outdated_with(env, fs, docnames, false)
853 }
854
855 #[test]
856 fn nothing_is_outdated_in_a_steady_state() {
857 let (env, fs) = steady_state();
858 let out = outdated(&env, &fs, &["index", "a"]);
859 assert_eq!(out, Outdated::default());
860 assert!(out.to_read().is_empty());
861 }
862
863 #[test]
864 fn a_document_the_environment_has_never_seen_is_added() {
865 let (env, mut fs) = steady_state();
866 fs.sources.insert("new".to_string(), Some(READ_TIME));
867 let out = outdated(&env, &fs, &["index", "a", "new"]);
868 assert_eq!(out.added, found(&["new"]));
869 assert!(out.changed.is_empty());
870 assert!(out.removed.is_empty());
871 }
872
873 #[test]
874 fn a_document_that_is_gone_is_removed() {
875 let (env, fs) = steady_state();
876 let out = outdated(&env, &fs, &["index"]);
877 assert_eq!(out.removed, found(&["a"]));
878 assert!(out.added.is_empty());
879 assert!(out.changed.is_empty());
882 }
883
884 #[test]
885 fn a_changed_configuration_re_reads_everything() {
886 let (env, fs) = steady_state();
887 let out = outdated_with(&env, &fs, &["index", "a"], true);
888 assert_eq!(out.added, found(&["index", "a"]));
889 assert!(
890 out.changed.is_empty(),
891 "sphinx puts every document in `added` and leaves `changed` empty"
892 );
893 }
894
895 #[test]
896 fn a_source_newer_than_its_read_time_has_changed() {
897 let (env, mut fs) = steady_state();
898 fs.sources.insert("a".to_string(), Some(READ_TIME + 1));
899 assert_eq!(outdated(&env, &fs, &["index", "a"]).changed, found(&["a"]));
900 }
901
902 #[test]
903 fn a_source_read_within_the_same_microsecond_has_not_changed() {
904 let (env, mut fs) = steady_state();
905 fs.sources.insert("a".to_string(), Some(READ_TIME));
906 assert!(
907 outdated(&env, &fs, &["index", "a"]).changed.is_empty(),
908 "the comparison is strictly-newer, like sphinx's"
909 );
910 }
911
912 #[test]
913 fn an_unstattable_source_has_changed() {
914 let (env, mut fs) = steady_state();
915 fs.sources.insert("a".to_string(), None);
916 assert_eq!(outdated(&env, &fs, &["index", "a"]).changed, found(&["a"]));
917 }
918
919 #[test]
920 fn a_missing_doctree_file_has_changed() {
921 let (env, mut fs) = steady_state();
922 fs.doctrees.remove("a");
923 assert_eq!(outdated(&env, &fs, &["index", "a"]).changed, found(&["a"]));
924 }
925
926 #[test]
927 fn a_document_that_asked_to_be_re_read_always_has_changed() {
928 let (mut env, fs) = steady_state();
929 env.reread_always.insert("a".to_string());
930 assert_eq!(outdated(&env, &fs, &["index", "a"]).changed, found(&["a"]));
931 }
932
933 #[test]
934 fn a_dependency_newer_than_the_read_time_has_changed() {
935 let (mut env, mut fs) = steady_state();
936 let pic = PathBuf::from("/src/pic.png");
937 env.dependencies
938 .insert("a".to_string(), BTreeSet::from([pic.clone()]));
939
940 fs.deps.insert(pic.clone(), Some(READ_TIME - 1));
941 assert!(outdated(&env, &fs, &["index", "a"]).changed.is_empty());
942
943 fs.deps.insert(pic, Some(READ_TIME + 1));
946 assert_eq!(outdated(&env, &fs, &["index", "a"]).changed, found(&["a"]));
947 }
948
949 #[test]
950 fn a_missing_dependency_has_changed() {
951 let (mut env, mut fs) = steady_state();
952 let pic = PathBuf::from("/src/pic.png");
953 env.dependencies
954 .insert("a".to_string(), BTreeSet::from([pic.clone()]));
955 fs.deps.insert(pic, None);
956 assert_eq!(outdated(&env, &fs, &["index", "a"]).changed, found(&["a"]));
957 }
958
959 #[test]
960 fn adding_or_removing_a_file_re_reads_every_glob_toctree() {
961 let (mut env, mut fs) = steady_state();
962 env.glob_toctrees.insert("index".to_string());
963 env.glob_toctrees.insert("gone".to_string());
966
967 assert!(outdated(&env, &fs, &["index", "a"]).changed.is_empty());
969
970 fs.sources.insert("new".to_string(), Some(READ_TIME));
971 fs.doctrees.insert("new".to_string());
972 let added = outdated(&env, &fs, &["index", "a", "new"]);
973 assert_eq!(added.added, found(&["new"]));
974 assert_eq!(added.changed, found(&["index"]));
975
976 let removed = outdated(&env, &fs, &["index"]);
977 assert_eq!(removed.removed, found(&["a"]));
978 assert_eq!(removed.changed, found(&["index"]));
979 }
980
981 #[test]
982 fn a_glob_container_that_is_new_itself_stays_in_added() {
983 let (mut env, mut fs) = steady_state();
984 env.glob_toctrees.insert("new".to_string());
985 fs.sources.insert("new".to_string(), Some(READ_TIME));
986 let out = outdated(&env, &fs, &["index", "a", "new"]);
987 assert_eq!(out.added, found(&["new"]));
988 assert!(
989 out.changed.is_empty(),
990 "a document is read once; being added already covers it"
991 );
992 }
993
994 #[test]
995 fn the_read_set_is_the_added_and_changed_documents() {
996 let (mut env, mut fs) = steady_state();
997 fs.sources.insert("new".to_string(), Some(READ_TIME));
998 fs.doctrees.remove("a");
999 env.all_docs.insert("gone".to_string(), READ_TIME);
1000
1001 let out = outdated(&env, &fs, &["index", "a", "new"]);
1002 assert_eq!(out.to_read(), found(&["a", "new"]));
1003 assert_eq!(out.removed, found(&["gone"]));
1004 }
1005
1006 #[test]
1007 fn snapshot_converts_tuple_keyed_maps_and_index_entry_main_flag() {
1008 let env = populated_env();
1009 let snapshot = env.snapshot();
1010
1011 let objects = snapshot["std"]["objects"].as_array().unwrap();
1012 assert_eq!(objects.len(), 1);
1013 assert_eq!(objects[0]["objtype"], "envvar");
1014 assert_eq!(objects[0]["name"], "PATH");
1015 assert_eq!(objects[0]["docname"], "index");
1016
1017 let progoptions = snapshot["std"]["progoptions"].as_array().unwrap();
1018 assert_eq!(progoptions[0]["program"], "myprog");
1019 assert_eq!(progoptions[0]["name"], "--verbose");
1020
1021 let entries = snapshot["index_entries"]["index"].as_array().unwrap();
1022 assert_eq!(entries.len(), 1);
1023 let entry = entries[0].as_array().unwrap();
1024 assert_eq!(entry[0], "single");
1025 assert_eq!(entry[3], "main"); let py_objects = snapshot["py_objects"].as_array().unwrap();
1029 assert_eq!(
1030 py_objects
1031 .iter()
1032 .map(|o| o["name"].as_str().unwrap())
1033 .collect::<Vec<_>>(),
1034 vec!["zeta.func", "alpha.func"]
1035 );
1036 assert_eq!(py_objects[1]["aliased"], true);
1037 let py_modules = snapshot["py_modules"].as_array().unwrap();
1038 assert_eq!(py_modules.len(), 1);
1039 assert_eq!(py_modules[0]["name"], "zeta");
1040 assert_eq!(py_modules[0]["node_id"], "module-zeta");
1041 assert_eq!(py_modules[0]["synopsis"], "Zed things.");
1042 assert_eq!(py_modules[0]["platform"], "posix");
1043 assert_eq!(py_modules[0]["deprecated"], true);
1044
1045 assert_eq!(
1046 snapshot["tocs_pformat"]["index"],
1047 JsonValue::String(sample_node().pformat())
1048 );
1049 }
1050}