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