1use anyhow::{Result, anyhow, bail, ensure};
45use serde::{Deserialize, Serialize};
46use std::fmt::Write;
47use std::sync::Arc;
48use ustr::{Ustr, UstrMap, UstrSet};
49
50use crate::{data::UnitType, error::UnitGraphError};
51
52pub trait UnitGraph {
60 fn add_course(&mut self, course_id: Ustr) -> Result<(), UnitGraphError>;
62
63 fn add_lesson(&mut self, lesson_id: Ustr, course_id: Ustr) -> Result<(), UnitGraphError>;
66
67 fn add_exercise(&mut self, exercise_id: Ustr, lesson_id: Ustr) -> Result<(), UnitGraphError>;
70
71 fn add_dependencies(
76 &mut self,
77 unit_id: Ustr,
78 unit_type: UnitType,
79 dependencies: &[Ustr],
80 ) -> Result<(), UnitGraphError>;
81
82 fn add_encompassed(
86 &mut self,
87 unit_id: Ustr,
88 dependencies: &[Ustr],
89 encompassed: &[(Ustr, f32)],
90 ) -> Result<(), UnitGraphError>;
91
92 fn set_encompasing_equals_dependency(&mut self);
98
99 fn encompasing_equals_dependency(&self) -> bool;
101
102 fn add_superseded(&mut self, unit_id: Ustr, superseded: &[Ustr]);
104
105 fn get_unit_type(&self, unit_id: Ustr) -> Option<UnitType>;
107
108 fn get_course_lessons(&self, course_id: Ustr) -> Option<Arc<UstrSet>>;
110
111 fn update_starting_lessons(&mut self);
117
118 fn get_starting_lessons(&self, course_id: Ustr) -> Option<Arc<UstrSet>>;
120
121 fn get_lesson_course(&self, lesson_id: Ustr) -> Option<Ustr>;
123
124 fn get_lesson_exercises(&self, lesson_id: Ustr) -> Option<Arc<UstrSet>>;
126
127 fn get_exercise_lesson(&self, exercise_id: Ustr) -> Option<Ustr>;
129
130 fn get_dependencies(&self, unit_id: Ustr) -> Option<Arc<UstrSet>>;
132
133 fn get_dependents(&self, unit_id: Ustr) -> Option<Arc<UstrSet>>;
135
136 fn get_dependency_sinks(&self) -> Arc<UstrSet>;
145
146 fn get_encompasses(&self, unit_id: Ustr) -> Option<Vec<(Ustr, f32)>>;
148
149 fn get_encompassed_by(&self, unit_id: Ustr) -> Option<Vec<(Ustr, f32)>>;
151
152 fn get_supersedes(&self, unit_id: Ustr) -> Option<Arc<UstrSet>>;
154
155 fn get_superseded_by(&self, unit_id: Ustr) -> Option<Arc<UstrSet>>;
157
158 fn check_cycles(&self) -> Result<(), UnitGraphError>;
161
162 fn generate_dot_graph(&self, courses_only: bool) -> String;
176}
177
178#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
182pub struct InMemoryUnitGraph {
183 type_map: UstrMap<UnitType>,
185
186 course_lesson_map: UstrMap<Arc<UstrSet>>,
188
189 starting_lessons_map: UstrMap<Arc<UstrSet>>,
191
192 lesson_course_map: UstrMap<Ustr>,
194
195 lesson_exercise_map: UstrMap<Arc<UstrSet>>,
197
198 exercise_lesson_map: UstrMap<Ustr>,
200
201 dependency_graph: UstrMap<Arc<UstrSet>>,
203
204 dependent_graph: UstrMap<Arc<UstrSet>>,
206
207 dependency_sinks: Arc<UstrSet>,
209
210 encompasses_graph: UstrMap<Vec<(Ustr, f32)>>,
212
213 encompassed_by: UstrMap<Vec<(Ustr, f32)>>,
215
216 supersedes_graph: UstrMap<Arc<UstrSet>>,
218
219 superseded_by: UstrMap<Arc<UstrSet>>,
221}
222
223impl InMemoryUnitGraph {
224 fn update_dependency_sinks(&mut self, unit_id: Ustr, dependencies: &[Ustr]) {
227 let empty = Arc::new(UstrSet::default());
230 let current_dependencies = self.dependency_graph.get(&unit_id).unwrap_or(&empty);
231 if current_dependencies.is_empty() && dependencies.is_empty() {
232 Arc::make_mut(&mut self.dependency_sinks).insert(unit_id);
233 } else {
234 Arc::make_mut(&mut self.dependency_sinks).remove(&unit_id);
235 }
236
237 if self.get_lesson_course(unit_id).is_some() {
241 Arc::make_mut(&mut self.dependency_sinks).remove(&unit_id);
242 }
243
244 for dependency_id in dependencies {
251 self.update_dependency_sinks(*dependency_id, &[]);
252 }
253 }
254
255 fn update_unit_type(&mut self, unit_id: Ustr, unit_type: UnitType) -> Result<()> {
258 match self.type_map.get(&unit_id) {
259 None => {
260 self.type_map.insert(unit_id, unit_type);
261 Ok(())
262 }
263 Some(existing_type) => {
264 if unit_type == *existing_type {
265 Ok(())
266 } else {
267 Err(anyhow!(
268 "cannot update unit type of unit {unit_id} from type {existing_type:#?}) \
269 to {unit_type:#?}.",
270 ))
271 }
272 }
273 }
274 }
275
276 fn add_course_helper(&mut self, course_id: Ustr) -> Result<()> {
278 ensure!(
280 !self.type_map.contains_key(&course_id),
281 "course with ID {course_id} already exists",
282 );
283
284 self.update_unit_type(course_id, UnitType::Course)?;
286 Ok(())
287 }
288
289 fn add_lesson_helper(&mut self, lesson_id: Ustr, course_id: Ustr) -> Result<()> {
291 ensure!(
293 !self.type_map.contains_key(&lesson_id),
294 "lesson with ID {lesson_id} already exists",
295 );
296
297 self.update_unit_type(lesson_id, UnitType::Lesson)?;
299 self.update_unit_type(course_id, UnitType::Course)?;
300
301 self.lesson_course_map.insert(lesson_id, course_id);
303 Arc::make_mut(self.course_lesson_map.entry(course_id).or_default()).insert(lesson_id);
304 Ok(())
305 }
306
307 fn add_exercise_helper(&mut self, exercise_id: Ustr, lesson_id: Ustr) -> Result<()> {
309 ensure!(
311 !self.type_map.contains_key(&exercise_id),
312 "exercise with ID {exercise_id} already exists",
313 );
314
315 self.update_unit_type(exercise_id, UnitType::Exercise)?;
317 self.update_unit_type(lesson_id, UnitType::Lesson)?;
318
319 Arc::make_mut(self.lesson_exercise_map.entry(lesson_id).or_default()).insert(exercise_id);
321 self.exercise_lesson_map.insert(exercise_id, lesson_id);
322 Ok(())
323 }
324
325 #[cfg_attr(coverage, coverage(off))]
327 fn verify_dependencies(
328 &self,
329 unit_id: Ustr,
330 unit_type: &UnitType,
331 dependencies: &[Ustr],
332 ) -> Result<()> {
333 ensure!(
334 *unit_type != UnitType::Exercise,
335 "exercise {unit_id} cannot have dependencies",
336 );
337 ensure!(
338 dependencies.iter().all(|dep| *dep != unit_id),
339 "unit {unit_id} cannot depend on itself",
340 );
341 ensure!(
342 self.type_map.contains_key(&unit_id),
343 "unit {unit_id} of type {unit_type:?} must be explicitly added before adding \
344 dependencies",
345 );
346 Ok(())
347 }
348
349 fn add_dependencies_helper(
351 &mut self,
352 unit_id: Ustr,
353 unit_type: &UnitType,
354 dependencies: &[Ustr],
355 ) -> Result<()> {
356 self.verify_dependencies(unit_id, unit_type, dependencies)?;
357
358 self.update_dependency_sinks(unit_id, dependencies);
360 Arc::make_mut(self.dependency_graph.entry(unit_id).or_default()).extend(dependencies);
361
362 for dependency_id in dependencies {
364 Arc::make_mut(self.dependent_graph.entry(*dependency_id).or_default()).insert(unit_id);
365 }
366 Ok(())
367 }
368
369 fn add_encompassed_helper(
371 &mut self,
372 unit_id: Ustr,
373 dependencies: &[Ustr],
374 encompassed: &[(Ustr, f32)],
375 ) -> Result<()> {
376 ensure!(
377 encompassed
378 .iter()
379 .all(|(_, weight)| (0.0..=1.0).contains(weight)),
380 "encompassed units of unit {unit_id} must have weights within the range [0.0, 1.0]",
381 );
382
383 let mut full_encompassed = encompassed.to_vec();
386 for dependency in dependencies {
387 if !encompassed
388 .iter()
389 .any(|(encompassed_id, _)| *encompassed_id == *dependency)
390 {
391 full_encompassed.push((*dependency, 1.0));
392 }
393 }
394
395 self.encompasses_graph
397 .entry(unit_id)
398 .or_default()
399 .extend(full_encompassed.clone());
400 for (encompassed_id, weight) in full_encompassed {
401 self.encompassed_by
402 .entry(encompassed_id)
403 .or_default()
404 .push((unit_id, weight));
405 }
406 Ok(())
407 }
408
409 fn check_graph_cycles(
412 graph_keys: &[Ustr],
413 get_neighbors: impl Fn(Ustr) -> Option<Vec<Ustr>>,
414 check_reverse: impl Fn(Ustr, Ustr) -> Result<()>,
415 cycle_msg: &str,
416 ) -> Result<()> {
417 let mut visited = UstrSet::default();
419 for unit_id in graph_keys {
420 if visited.contains(unit_id) {
421 continue;
422 }
423
424 let mut stack: Vec<Vec<Ustr>> = vec![vec![*unit_id]];
426 while let Some(path) = stack.pop() {
427 let current_id = *path.last().unwrap_or(&Ustr::default());
429 if visited.contains(¤t_id) {
430 continue;
431 }
432 visited.insert(current_id);
433
434 if let Some(neighbors) = get_neighbors(current_id) {
437 for neighbor_id in neighbors {
438 check_reverse(current_id, neighbor_id)?;
439 if path.contains(&neighbor_id) {
440 let mut cycle_path = path.clone();
441 cycle_path.push(neighbor_id);
442 let path_str = cycle_path.iter().fold(String::new(), |mut s, id| {
443 if !s.is_empty() {
444 s.push_str(" -> ");
445 }
446 let _ = write!(s, "{id}");
447 s
448 });
449 bail!("{cycle_msg}: {path_str}");
450 }
451 let mut new_path = path.clone();
452 new_path.push(neighbor_id);
453 stack.push(new_path);
454 }
455 }
456 }
457 }
458 Ok(())
459 }
460
461 fn check_cycles_helper(&self) -> Result<()> {
463 let dep_keys: Vec<Ustr> = self.dependency_graph.keys().copied().collect();
465 Self::check_graph_cycles(
466 &dep_keys,
467 |id| {
468 self.get_dependencies(id)
469 .map(|s| s.iter().copied().collect())
470 },
471 |current, dep| {
472 let dependents = self.get_dependents(dep).unwrap_or_default();
473 ensure!(
474 dependents.contains(¤t),
475 "unit {current} lists unit {dep} as a dependency but the dependent \
476 relationship does not exist",
477 );
478 Ok(())
479 },
480 "cycle in dependency graph detected",
481 )?;
482
483 let sup_keys: Vec<Ustr> = self.supersedes_graph.keys().copied().collect();
485 Self::check_graph_cycles(
486 &sup_keys,
487 |id| self.get_supersedes(id).map(|s| s.iter().copied().collect()),
488 |current, sup| {
489 let superseding = self.get_superseded_by(sup).unwrap_or_default();
490 ensure!(
491 superseding.contains(¤t),
492 "unit {current} lists unit {sup} as a superseded unit but the superseding \
493 relationship does not exist",
494 );
495 Ok(())
496 },
497 "cycle in superseded graph detected",
498 )?;
499
500 let enc_keys: Vec<Ustr> = self.encompasses_graph.keys().copied().collect();
502 Self::check_graph_cycles(
503 &enc_keys,
504 |id| {
505 self.get_encompasses(id)
506 .map(|v| v.iter().map(|(id, _)| *id).collect())
507 },
508 |current, enc| {
509 let encompassing = self.get_encompassed_by(enc).unwrap_or_default();
510 ensure!(
511 encompassing.iter().any(|(u, _)| *u == current),
512 "unit {current} lists unit {enc} as an encompassed unit but the encompassing \
513 relationship does not exist",
514 );
515 Ok(())
516 },
517 "cycle in encompassed graph detected",
518 )?;
519 Ok(())
520 }
521}
522
523impl UnitGraph for InMemoryUnitGraph {
524 fn add_course(&mut self, course_id: Ustr) -> Result<(), UnitGraphError> {
525 self.add_course_helper(course_id)
526 .map_err(|e| UnitGraphError::AddUnit(course_id, UnitType::Course, e))
527 }
528
529 fn add_lesson(&mut self, lesson_id: Ustr, course_id: Ustr) -> Result<(), UnitGraphError> {
530 self.add_lesson_helper(lesson_id, course_id)
531 .map_err(|e| UnitGraphError::AddUnit(lesson_id, UnitType::Lesson, e))
532 }
533
534 fn add_exercise(&mut self, exercise_id: Ustr, lesson_id: Ustr) -> Result<(), UnitGraphError> {
535 self.add_exercise_helper(exercise_id, lesson_id)
536 .map_err(|e| UnitGraphError::AddUnit(exercise_id, UnitType::Exercise, e))
537 }
538
539 fn add_dependencies(
540 &mut self,
541 unit_id: Ustr,
542 unit_type: UnitType,
543 dependencies: &[Ustr],
544 ) -> Result<(), UnitGraphError> {
545 self.add_dependencies_helper(unit_id, &unit_type, dependencies)
546 .map_err(|e| UnitGraphError::AddDependencies(unit_id, unit_type, e))
547 }
548
549 fn add_encompassed(
550 &mut self,
551 unit_id: Ustr,
552 dependencies: &[Ustr],
553 encompassed: &[(Ustr, f32)],
554 ) -> Result<(), UnitGraphError> {
555 self.add_encompassed_helper(unit_id, dependencies, encompassed)
556 .map_err(|e| UnitGraphError::AddEncompassed(unit_id, e))
557 }
558
559 fn set_encompasing_equals_dependency(&mut self) {
560 self.encompasses_graph.clear();
562 self.encompassed_by.clear();
563 }
564
565 fn encompasing_equals_dependency(&self) -> bool {
566 self.encompasses_graph.is_empty() && self.encompassed_by.is_empty()
567 }
568
569 fn add_superseded(&mut self, unit_id: Ustr, superseded: &[Ustr]) {
570 if superseded.is_empty() {
572 return;
573 }
574 Arc::make_mut(self.supersedes_graph.entry(unit_id).or_default()).extend(superseded);
575
576 for superseded_id in superseded {
578 Arc::make_mut(self.superseded_by.entry(*superseded_id).or_default()).insert(unit_id);
579 }
580 }
581
582 fn get_unit_type(&self, unit_id: Ustr) -> Option<UnitType> {
583 self.type_map.get(&unit_id).cloned()
584 }
585
586 fn get_course_lessons(&self, course_id: Ustr) -> Option<Arc<UstrSet>> {
587 self.course_lesson_map.get(&course_id).cloned()
588 }
589
590 fn get_starting_lessons(&self, course_id: Ustr) -> Option<Arc<UstrSet>> {
591 self.starting_lessons_map.get(&course_id).cloned()
592 }
593
594 fn update_starting_lessons(&mut self) {
595 let empty = Arc::new(UstrSet::default());
597 for course_id in self.course_lesson_map.keys() {
598 let lessons = self.course_lesson_map.get(course_id).unwrap_or(&empty);
599 let starting_lessons: UstrSet = lessons
600 .iter()
601 .copied()
602 .filter(|lesson_id| {
603 let dependencies = self.get_dependencies(*lesson_id);
606 match dependencies {
607 None => true,
608 Some(dependencies) => lessons.is_disjoint(&dependencies),
609 }
610 })
611 .collect();
612
613 if self.dependency_sinks.contains(course_id) {
617 let has_starting_dependencies = starting_lessons.iter().any(|lesson_id| {
618 self.get_dependencies(*lesson_id)
619 .is_some_and(|dependencies| {
620 !dependencies.is_empty()
621 && dependencies
622 .iter()
623 .all(|dep| self.get_unit_type(*dep).is_some())
624 })
625 });
626 if has_starting_dependencies {
627 Arc::make_mut(&mut self.dependency_sinks).remove(course_id);
628 }
629 }
630 self.starting_lessons_map
631 .insert(*course_id, Arc::new(starting_lessons));
632 }
633 }
634
635 fn get_lesson_course(&self, lesson_id: Ustr) -> Option<Ustr> {
636 self.lesson_course_map.get(&lesson_id).copied()
637 }
638
639 fn get_lesson_exercises(&self, lesson_id: Ustr) -> Option<Arc<UstrSet>> {
640 self.lesson_exercise_map.get(&lesson_id).cloned()
641 }
642
643 fn get_exercise_lesson(&self, exercise_id: Ustr) -> Option<Ustr> {
644 self.exercise_lesson_map.get(&exercise_id).copied()
645 }
646
647 fn get_dependencies(&self, unit_id: Ustr) -> Option<Arc<UstrSet>> {
648 self.dependency_graph.get(&unit_id).cloned()
649 }
650
651 fn get_dependents(&self, unit_id: Ustr) -> Option<Arc<UstrSet>> {
652 self.dependent_graph.get(&unit_id).cloned()
653 }
654
655 fn get_dependency_sinks(&self) -> Arc<UstrSet> {
656 self.dependency_sinks.clone()
657 }
658
659 fn get_encompasses(&self, unit_id: Ustr) -> Option<Vec<(Ustr, f32)>> {
660 if self.encompasses_graph.is_empty() {
662 self.get_dependencies(unit_id)
663 .map(|dependencies| dependencies.iter().copied().map(|dep| (dep, 1.0)).collect())
664 } else {
665 self.encompasses_graph.get(&unit_id).cloned()
666 }
667 }
668
669 fn get_encompassed_by(&self, unit_id: Ustr) -> Option<Vec<(Ustr, f32)>> {
670 if self.encompassed_by.is_empty() {
672 self.get_dependents(unit_id)
673 .map(|dependents| dependents.iter().copied().map(|dep| (dep, 1.0)).collect())
674 } else {
675 self.encompassed_by.get(&unit_id).cloned()
676 }
677 }
678
679 fn get_supersedes(&self, unit_id: Ustr) -> Option<Arc<UstrSet>> {
680 self.supersedes_graph.get(&unit_id).cloned()
681 }
682
683 fn get_superseded_by(&self, unit_id: Ustr) -> Option<Arc<UstrSet>> {
684 self.superseded_by.get(&unit_id).cloned()
685 }
686
687 fn check_cycles(&self) -> Result<(), UnitGraphError> {
688 self.check_cycles_helper()
689 .map_err(UnitGraphError::CheckCycles)
690 }
691
692 fn generate_dot_graph(&self, courses_only: bool) -> String {
693 let mut output = String::from("digraph dependent_graph {\n");
695 let mut courses = self.course_lesson_map.keys().copied().collect::<Vec<_>>();
696 courses.sort();
697
698 for course_id in courses {
700 let _ = writeln!(output, " \"{course_id}\" [color=red, style=filled]");
702
703 let mut dependents = self
706 .get_dependents(course_id)
707 .unwrap_or_default()
708 .iter()
709 .copied()
710 .collect::<Vec<_>>();
711 dependents.retain(|dependent_id| {
712 if courses_only {
713 self.get_unit_type(*dependent_id) == Some(UnitType::Course)
714 } else {
715 true
716 }
717 });
718
719 if !courses_only {
729 dependents.extend(
730 self.get_starting_lessons(course_id)
731 .unwrap_or_default()
732 .iter(),
733 );
734 }
735
736 dependents.sort();
738 for dependent in dependents {
739 let _ = writeln!(output, " \"{course_id}\" -> \"{dependent}\"");
740 }
741
742 if courses_only {
745 continue;
746 }
747 let mut lessons = self
748 .get_course_lessons(course_id)
749 .unwrap_or_default()
750 .iter()
751 .copied()
752 .collect::<Vec<_>>();
753 lessons.sort();
754 for lesson_id in lessons {
755 let _ = writeln!(output, " \"{lesson_id}\" [color=blue, style=filled]");
757
758 let mut dependents = self
760 .get_dependents(lesson_id)
761 .unwrap_or_default()
762 .iter()
763 .copied()
764 .collect::<Vec<_>>();
765 dependents.sort();
766 for dependent in dependents {
767 let _ = writeln!(output, " \"{lesson_id}\" -> \"{dependent}\"");
768 }
769 }
770 }
771
772 output.push_str("}\n");
774 output
775 }
776}
777
778#[cfg(test)]
779#[cfg_attr(coverage, coverage(off))]
780mod test {
781 use anyhow::Result;
782 use indoc::indoc;
783 use std::sync::Arc;
784 use ustr::{Ustr, UstrSet};
785
786 use crate::{
787 data::UnitType,
788 graph::{InMemoryUnitGraph, UnitGraph},
789 };
790
791 #[test]
793 fn get_unit_type() -> Result<()> {
794 let mut graph = InMemoryUnitGraph::default();
795 let id = Ustr::from("id1");
796 graph.add_course(id)?;
797 graph.add_dependencies(id, UnitType::Course, &[])?;
798 assert_eq!(graph.get_unit_type(id), Some(UnitType::Course));
799 Ok(())
800 }
801
802 #[test]
804 fn get_course_lessons_and_exercises() -> Result<()> {
805 let mut graph = InMemoryUnitGraph::default();
806 let course_id = Ustr::from("course1");
807 let lesson1_id = Ustr::from("course1::lesson1");
808 let lesson2_id = Ustr::from("course1::lesson2");
809 let lesson1_exercise1_id = Ustr::from("course1::lesson1::exercise1");
810 let lesson1_exercise2_id = Ustr::from("course1::lesson1::exercise2");
811 let lesson2_exercise1_id = Ustr::from("course1::lesson2::exercise1");
812 let lesson2_exercise2_id = Ustr::from("course1::lesson2::exercise2");
813
814 graph.add_course(course_id)?;
815 graph.add_dependencies(course_id, UnitType::Course, &[])?;
816 graph.add_lesson(lesson1_id, course_id)?;
817 graph.add_exercise(lesson1_exercise1_id, lesson1_id)?;
818 graph.add_exercise(lesson1_exercise2_id, lesson1_id)?;
819 graph.add_lesson(lesson2_id, course_id)?;
820 graph.add_exercise(lesson2_exercise1_id, lesson2_id)?;
821 graph.add_exercise(lesson2_exercise2_id, lesson2_id)?;
822
823 let course_lessons = graph.get_course_lessons(course_id).unwrap();
824 assert_eq!(course_lessons.len(), 2);
825 assert!(course_lessons.contains(&lesson1_id));
826 assert!(course_lessons.contains(&lesson2_id));
827
828 let lesson1_exercises = graph.get_lesson_exercises(lesson1_id).unwrap();
829 assert_eq!(lesson1_exercises.len(), 2);
830 assert!(lesson1_exercises.contains(&lesson1_exercise1_id));
831 assert!(lesson1_exercises.contains(&lesson1_exercise2_id));
832 assert_eq!(
833 graph.get_exercise_lesson(lesson1_exercise1_id).unwrap(),
834 lesson1_id
835 );
836 assert_eq!(
837 graph.get_exercise_lesson(lesson1_exercise2_id).unwrap(),
838 lesson1_id
839 );
840
841 let lesson2_exercises = graph.get_lesson_exercises(lesson2_id).unwrap();
842 assert_eq!(lesson2_exercises.len(), 2);
843 assert!(lesson2_exercises.contains(&lesson2_exercise1_id));
844 assert!(lesson2_exercises.contains(&lesson2_exercise2_id));
845 assert_eq!(
846 graph.get_exercise_lesson(lesson2_exercise1_id).unwrap(),
847 lesson2_id
848 );
849 assert_eq!(
850 graph.get_exercise_lesson(lesson2_exercise2_id).unwrap(),
851 lesson2_id
852 );
853
854 Ok(())
855 }
856
857 #[test]
859 fn dependency_graph() -> Result<()> {
860 let mut graph = InMemoryUnitGraph::default();
861 let course1_id = Ustr::from("course1");
862 let course2_id = Ustr::from("course2");
863 let course3_id = Ustr::from("course3");
864 let course4_id = Ustr::from("course4");
865 let course5_id = Ustr::from("course5");
866 graph.add_course(course1_id)?;
867 graph.add_course(course2_id)?;
868 graph.add_course(course3_id)?;
869 graph.add_course(course4_id)?;
870 graph.add_course(course5_id)?;
871 graph.add_dependencies(course1_id, UnitType::Course, &[])?;
872 graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
873 graph.add_dependencies(course3_id, UnitType::Course, &[course1_id])?;
874 graph.add_dependencies(course4_id, UnitType::Course, &[course2_id])?;
875 graph.add_dependencies(course5_id, UnitType::Course, &[course3_id])?;
876
877 {
878 let dependents = graph.get_dependents(course1_id).unwrap();
879 assert_eq!(dependents.len(), 2);
880 assert!(dependents.contains(&course2_id));
881 assert!(dependents.contains(&course3_id));
882 assert!(graph.get_dependencies(course1_id).unwrap().is_empty());
883 }
884
885 {
886 let dependents = graph.get_dependents(course2_id).unwrap();
887 assert_eq!(dependents.len(), 1);
888 assert!(dependents.contains(&course4_id));
889 let dependencies = graph.get_dependencies(course2_id).unwrap();
890 assert_eq!(dependencies.len(), 1);
891 assert!(dependencies.contains(&course1_id));
892 }
893
894 {
895 let dependents = graph.get_dependents(course3_id).unwrap();
896 assert_eq!(dependents.len(), 1);
897 assert!(dependents.contains(&course5_id));
898 let dependencies = graph.get_dependencies(course3_id).unwrap();
899 assert_eq!(dependencies.len(), 1);
900 assert!(dependencies.contains(&course1_id));
901 }
902
903 {
904 assert!(graph.get_dependents(course4_id).is_none());
905 let dependencies = graph.get_dependencies(course4_id).unwrap();
906 assert_eq!(dependencies.len(), 1);
907 assert!(dependencies.contains(&course2_id));
908 }
909
910 {
911 assert!(graph.get_dependents(course5_id).is_none());
912 let dependencies = graph.get_dependencies(course5_id).unwrap();
913 assert_eq!(dependencies.len(), 1);
914 assert!(dependencies.contains(&course3_id));
915 }
916
917 let sinks = graph.get_dependency_sinks();
918 assert_eq!(sinks.len(), 1);
919 assert!(sinks.contains(&course1_id));
920
921 graph.check_cycles()?;
922 Ok(())
923 }
924
925 #[test]
928 fn courses_with_starting_dependencies_not_in_sinks() -> Result<()> {
929 let mut graph = InMemoryUnitGraph::default();
930 let course1_id = Ustr::from("course1");
931 let course2_id = Ustr::from("course2");
932 let course2_lesson_1_id = Ustr::from("course2::lesson1");
933 graph.add_course(course1_id)?;
934 graph.add_course(course2_id)?;
935 graph.add_lesson(course2_lesson_1_id, course2_id)?;
936 graph.add_dependencies(course1_id, UnitType::Course, &[])?;
937 graph.add_dependencies(course2_id, UnitType::Course, &[])?;
938 graph.add_dependencies(course2_lesson_1_id, UnitType::Lesson, &[course1_id])?;
939 graph.update_starting_lessons();
940
941 let sinks = graph.get_dependency_sinks();
942 assert_eq!(*sinks, vec![course1_id].into_iter().collect::<UstrSet>());
943 Ok(())
944 }
945
946 #[test]
948 fn encompassing_graph() -> Result<()> {
949 let mut graph = InMemoryUnitGraph::default();
950 let course1_id = Ustr::from("course1");
951 let course2_id = Ustr::from("course2");
952 let course3_id = Ustr::from("course3");
953 graph.add_course(course1_id)?;
954 graph.add_course(course2_id)?;
955 graph.add_course(course3_id)?;
956 graph.add_dependencies(course1_id, UnitType::Course, &[])?;
957 graph.add_encompassed(course1_id, &[], &[])?;
958 graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
959 graph.add_encompassed(course2_id, &[course1_id], &[])?;
960 graph.add_dependencies(course3_id, UnitType::Course, &[course1_id])?;
961 graph.add_encompassed(
962 course3_id,
963 &[course1_id],
964 &[(course1_id, 0.5), (course2_id, 0.5)],
965 )?;
966
967 assert!(!graph.encompasing_equals_dependency());
968 {
969 let encompassed = graph.get_encompasses(course1_id).unwrap();
970 assert_eq!(encompassed.len(), 0);
971 let encompassed_by = graph.get_encompassed_by(course1_id).unwrap();
972 assert_eq!(encompassed_by.len(), 2);
973 assert!(encompassed_by.contains(&(course3_id, 0.5)));
974 assert!(encompassed_by.contains(&(course2_id, 1.0)));
975 }
976
977 {
978 let encompassed = graph.get_encompasses(course3_id).unwrap();
979 assert_eq!(encompassed.len(), 2);
980 assert!(encompassed.contains(&(course1_id, 0.5)));
981 assert!(encompassed.contains(&(course2_id, 0.5)));
982 let encompassed_by = graph.get_encompassed_by(course2_id).unwrap();
983 assert_eq!(encompassed_by.len(), 1);
984 assert!(encompassed_by.contains(&(course3_id, 0.5)));
985 }
986
987 {
988 let encompassed = graph.get_encompasses(course2_id).unwrap();
989 assert_eq!(encompassed.len(), 1);
990 assert!(encompassed.contains(&(course1_id, 1.0)));
991 let encompassed_by = graph.get_encompassed_by(course3_id);
992 assert!(encompassed_by.is_none());
993 }
994 Ok(())
995 }
996
997 #[test]
999 fn encompassing_equals_dependencies() -> Result<()> {
1000 let mut graph = InMemoryUnitGraph::default();
1001 let course1_id = Ustr::from("course1");
1002 let course2_id = Ustr::from("course2");
1003 let course3_id = Ustr::from("course3");
1004 graph.add_course(course1_id)?;
1005 graph.add_course(course2_id)?;
1006 graph.add_course(course3_id)?;
1007 graph.add_dependencies(course1_id, UnitType::Course, &[])?;
1008 graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
1009 graph.add_dependencies(course3_id, UnitType::Course, &[course1_id])?;
1010
1011 assert!(graph.encompasing_equals_dependency());
1012 {
1013 let encompassed = graph.get_encompasses(course1_id).unwrap();
1014 assert_eq!(encompassed.len(), 0);
1015 let dependencies = graph.get_dependencies(course1_id).unwrap();
1016 assert_eq!(dependencies.len(), 0);
1017
1018 let encompassed_by = graph.get_encompassed_by(course1_id).unwrap();
1019 assert_eq!(encompassed_by.len(), 2);
1020 assert!(encompassed_by.contains(&(course2_id, 1.0)));
1021 assert!(encompassed_by.contains(&(course3_id, 1.0)));
1022 let dependents = graph.get_dependents(course1_id).unwrap();
1023 assert_eq!(dependents.len(), 2);
1024 assert!(dependents.contains(&course2_id));
1025 assert!(dependents.contains(&course3_id));
1026 }
1027
1028 {
1029 let encompassed = graph.get_encompasses(course2_id).unwrap();
1030 assert_eq!(encompassed.len(), 1);
1031 assert!(encompassed.contains(&(course1_id, 1.0)));
1032 let dependencies = graph.get_dependencies(course2_id).unwrap();
1033 assert_eq!(dependencies.len(), 1);
1034 assert!(dependencies.contains(&course1_id));
1035
1036 let encompassed_by = graph.get_encompassed_by(course2_id);
1037 assert!(encompassed_by.is_none());
1038 let dependents = graph.get_dependents(course2_id);
1039 assert!(dependents.is_none());
1040 }
1041
1042 {
1043 let encompassed = graph.get_encompasses(course3_id).unwrap();
1044 assert_eq!(encompassed.len(), 1);
1045 assert!(encompassed.contains(&(course1_id, 1.0)));
1046 let dependencies = graph.get_dependencies(course3_id).unwrap();
1047 assert_eq!(dependencies.len(), 1);
1048 assert!(dependencies.contains(&course1_id));
1049
1050 let encompassed_by = graph.get_encompassed_by(course3_id);
1051 assert!(encompassed_by.is_none());
1052 let dependents = graph.get_dependents(course3_id);
1053 assert!(dependents.is_none());
1054 }
1055 Ok(())
1056 }
1057
1058 #[test]
1060 fn superseding_graph() -> Result<()> {
1061 let mut graph = InMemoryUnitGraph::default();
1062 let course1_id = Ustr::from("course1");
1063 let course2_id = Ustr::from("course2");
1064 let course3_id = Ustr::from("course3");
1065 graph.add_course(course1_id)?;
1066 graph.add_course(course2_id)?;
1067 graph.add_course(course3_id)?;
1068 graph.add_dependencies(course1_id, UnitType::Course, &[])?;
1069 graph.add_superseded(course1_id, &[course2_id]);
1070 graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
1071 graph.add_superseded(course2_id, &[course3_id]);
1072 graph.add_dependencies(course3_id, UnitType::Course, &[course2_id])?;
1073
1074 {
1075 let superseded = graph.get_supersedes(course1_id).unwrap();
1076 assert_eq!(superseded.len(), 1);
1077 assert!(superseded.contains(&course2_id));
1078 assert!(graph.get_superseded_by(course1_id).is_none());
1079 }
1080
1081 {
1082 let superseded = graph.get_supersedes(course2_id).unwrap();
1083 assert_eq!(superseded.len(), 1);
1084 assert!(superseded.contains(&course3_id));
1085 let superseded_by = graph.get_superseded_by(course2_id).unwrap();
1086 assert_eq!(superseded_by.len(), 1);
1087 assert!(superseded_by.contains(&course1_id));
1088 }
1089
1090 {
1091 assert!(graph.get_supersedes(course3_id).is_none());
1092 let superseded_by = graph.get_superseded_by(course3_id).unwrap();
1093 assert_eq!(superseded_by.len(), 1);
1094 assert!(superseded_by.contains(&course2_id));
1095 }
1096 Ok(())
1097 }
1098
1099 #[test]
1101 fn generate_dot_graph() -> Result<()> {
1102 let mut graph = InMemoryUnitGraph::default();
1103 let course1_id = Ustr::from("1");
1104 let course1_lesson1_id = Ustr::from("1::1");
1105 let course1_lesson2_id = Ustr::from("1::2");
1106 let course2_id = Ustr::from("2");
1107 let course2_lesson1_id = Ustr::from("2::1");
1108 let course3_id = Ustr::from("3");
1109 let course3_lesson1_id = Ustr::from("3::1");
1110 let course3_lesson2_id = Ustr::from("3::2");
1111
1112 graph.add_lesson(course1_lesson1_id, course1_id)?;
1113 graph.add_lesson(course1_lesson2_id, course1_id)?;
1114 graph.add_lesson(course2_lesson1_id, course2_id)?;
1115 graph.add_lesson(course3_lesson1_id, course3_id)?;
1116 graph.add_lesson(course3_lesson2_id, course3_id)?;
1117
1118 graph.add_dependencies(course1_id, UnitType::Course, &[])?;
1119 graph.add_dependencies(course1_lesson2_id, UnitType::Lesson, &[course1_lesson1_id])?;
1120 graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
1121 graph.add_dependencies(course3_id, UnitType::Course, &[course2_id])?;
1122 graph.add_dependencies(course3_lesson2_id, UnitType::Lesson, &[course3_lesson1_id])?;
1123 graph.update_starting_lessons();
1124
1125 let dot = graph.generate_dot_graph(false);
1127 let expected = indoc! {r#"
1128 digraph dependent_graph {
1129 "1" [color=red, style=filled]
1130 "1" -> "1::1"
1131 "1" -> "2"
1132 "1::1" [color=blue, style=filled]
1133 "1::1" -> "1::2"
1134 "1::2" [color=blue, style=filled]
1135 "2" [color=red, style=filled]
1136 "2" -> "2::1"
1137 "2" -> "3"
1138 "2::1" [color=blue, style=filled]
1139 "3" [color=red, style=filled]
1140 "3" -> "3::1"
1141 "3::1" [color=blue, style=filled]
1142 "3::1" -> "3::2"
1143 "3::2" [color=blue, style=filled]
1144 }
1145 "#};
1146 assert_eq!(dot, expected);
1147
1148 let dot_courses_only = graph.generate_dot_graph(true);
1150 let expected_courses_only = indoc! {r#"
1151 digraph dependent_graph {
1152 "1" [color=red, style=filled]
1153 "1" -> "2"
1154 "2" [color=red, style=filled]
1155 "2" -> "3"
1156 "3" [color=red, style=filled]
1157 }
1158 "#};
1159 assert_eq!(dot_courses_only, expected_courses_only);
1160 Ok(())
1161 }
1162
1163 #[test]
1164 fn duplicate_ids() -> Result<()> {
1165 let mut graph = InMemoryUnitGraph::default();
1166
1167 let course_id = Ustr::from("course_id");
1168 graph.add_course(course_id)?;
1169 let _ = graph.add_course(course_id).is_err();
1170
1171 let lesson_id = Ustr::from("lesson_id");
1172 graph.add_lesson(lesson_id, course_id)?;
1173 let _ = graph.add_lesson(lesson_id, course_id).is_err();
1174
1175 let exercise_id = Ustr::from("exercise_id");
1176 graph.add_exercise(exercise_id, lesson_id)?;
1177 let _ = graph.add_exercise(exercise_id, lesson_id).is_err();
1178
1179 Ok(())
1180 }
1181
1182 #[test]
1183 fn update_unit_type_different_types() -> Result<()> {
1184 let mut graph = InMemoryUnitGraph::default();
1185 let unit_id = Ustr::from("unit_id");
1186 graph.update_unit_type(unit_id, UnitType::Course)?;
1187 assert!(graph.update_unit_type(unit_id, UnitType::Lesson).is_err());
1188 Ok(())
1189 }
1190
1191 #[test]
1193 fn dependencies_cycle() -> Result<()> {
1194 let mut graph = InMemoryUnitGraph::default();
1195 let course1_id = Ustr::from("course1");
1196 let course2_id = Ustr::from("course2");
1197 let course3_id = Ustr::from("course3");
1198 let course4_id = Ustr::from("course4");
1199 let course5_id = Ustr::from("course5");
1200 graph.add_course(course1_id)?;
1201 graph.add_course(course2_id)?;
1202 graph.add_course(course3_id)?;
1203 graph.add_course(course4_id)?;
1204 graph.add_course(course5_id)?;
1205 graph.add_dependencies(course1_id, UnitType::Course, &[])?;
1206 graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
1207 graph.add_dependencies(course3_id, UnitType::Course, &[course1_id])?;
1208 graph.add_dependencies(course4_id, UnitType::Course, &[course2_id])?;
1209 graph.add_dependencies(course5_id, UnitType::Course, &[course3_id])?;
1210
1211 graph.add_dependencies(course1_id, UnitType::Course, &[course5_id])?;
1213 assert!(graph.check_cycles().is_err());
1214
1215 Ok(())
1216 }
1217
1218 #[test]
1220 fn encompassed_cycle() -> Result<()> {
1221 let mut graph = InMemoryUnitGraph::default();
1223 let course1_id = Ustr::from("course1");
1224 let course2_id = Ustr::from("course2");
1225 let course3_id = Ustr::from("course3");
1226 let course4_id = Ustr::from("course4");
1227 let course5_id = Ustr::from("course5");
1228 graph.add_encompassed(course2_id, &[course1_id], &[])?;
1229 graph.add_encompassed(course3_id, &[course1_id], &[])?;
1230 graph.add_encompassed(course4_id, &[course2_id], &[])?;
1231 graph.add_encompassed(course5_id, &[course3_id], &[])?;
1232 graph.add_encompassed(course1_id, &[course5_id], &[])?;
1233 assert!(graph.check_cycles().is_err());
1234 Ok(())
1235 }
1236
1237 #[test]
1239 fn superseded_cycle() {
1240 let mut graph = InMemoryUnitGraph::default();
1242 let course1_id = Ustr::from("course1");
1243 let course2_id = Ustr::from("course2");
1244 let course3_id = Ustr::from("course3");
1245 let course4_id = Ustr::from("course4");
1246 let course5_id = Ustr::from("course5");
1247 graph.add_superseded(course2_id, &[course1_id]);
1248 graph.add_superseded(course3_id, &[course1_id]);
1249 graph.add_superseded(course4_id, &[course2_id]);
1250 graph.add_superseded(course5_id, &[course3_id]);
1251 graph.add_superseded(course1_id, &[course5_id]);
1252 assert!(graph.check_cycles().is_err());
1253 }
1254
1255 #[test]
1257 fn missing_dependent_relationship() -> Result<()> {
1258 let mut graph = InMemoryUnitGraph::default();
1259 let course_id = Ustr::from("course_id");
1260 let lesson1_id = Ustr::from("lesson1_id");
1261 let lesson2_id = Ustr::from("lesson2_id");
1262 graph.add_course(course_id).unwrap();
1263 graph.add_lesson(lesson1_id, course_id).unwrap();
1264 graph.add_lesson(lesson2_id, course_id).unwrap();
1265 graph.add_dependencies(lesson2_id, UnitType::Lesson, &[lesson1_id])?;
1266
1267 graph
1270 .dependent_graph
1271 .insert(lesson1_id, Arc::new(UstrSet::default()));
1272 assert!(graph.check_cycles().is_err());
1273 graph.dependency_graph.remove(&lesson1_id);
1275 assert!(graph.check_cycles().is_err());
1276 Ok(())
1277 }
1278
1279 #[test]
1281 fn missing_encompasing_relationship() -> Result<()> {
1282 let mut graph = InMemoryUnitGraph::default();
1283 let lesson1_id = Ustr::from("lesson1_id");
1284 let lesson2_id = Ustr::from("lesson2_id");
1285 graph.add_encompassed(lesson2_id, &[lesson1_id], &[])?;
1286
1287 graph.encompassed_by.insert(lesson1_id, Vec::default());
1290 assert!(graph.check_cycles().is_err());
1291 graph.encompassed_by.remove(&lesson1_id);
1293 assert!(graph.check_cycles().is_err());
1294 Ok(())
1295 }
1296
1297 #[test]
1299 fn missing_superseding_relationship() {
1300 let mut graph = InMemoryUnitGraph::default();
1301 let lesson1_id = Ustr::from("lesson1_id");
1302 let lesson2_id = Ustr::from("lesson2_id");
1303 graph.add_superseded(lesson2_id, &[lesson1_id]);
1304
1305 graph
1308 .superseded_by
1309 .insert(lesson1_id, Arc::new(UstrSet::default()));
1310 assert!(graph.check_cycles().is_err());
1311 graph.dependency_graph.remove(&lesson1_id);
1313 assert!(graph.check_cycles().is_err());
1314 }
1315}