use anyhow::{Result, anyhow, bail, ensure};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
use std::sync::Arc;
use ustr::{Ustr, UstrMap, UstrSet};
use crate::{data::UnitType, error::UnitGraphError};
pub trait UnitGraph {
fn add_course(&mut self, course_id: Ustr) -> Result<(), UnitGraphError>;
fn add_lesson(&mut self, lesson_id: Ustr, course_id: Ustr) -> Result<(), UnitGraphError>;
fn add_exercise(&mut self, exercise_id: Ustr, lesson_id: Ustr) -> Result<(), UnitGraphError>;
fn add_dependencies(
&mut self,
unit_id: Ustr,
unit_type: UnitType,
dependencies: &[Ustr],
) -> Result<(), UnitGraphError>;
fn add_encompassed(
&mut self,
unit_id: Ustr,
dependencies: &[Ustr],
encompassed: &[(Ustr, f32)],
) -> Result<(), UnitGraphError>;
fn set_encompasing_equals_dependency(&mut self);
fn encompasing_equals_dependency(&self) -> bool;
fn add_superseded(&mut self, unit_id: Ustr, superseded: &[Ustr]);
fn get_unit_type(&self, unit_id: Ustr) -> Option<UnitType>;
fn get_course_lessons(&self, course_id: Ustr) -> Option<Arc<UstrSet>>;
fn update_starting_lessons(&mut self);
fn get_starting_lessons(&self, course_id: Ustr) -> Option<Arc<UstrSet>>;
fn get_lesson_course(&self, lesson_id: Ustr) -> Option<Ustr>;
fn get_lesson_exercises(&self, lesson_id: Ustr) -> Option<Arc<UstrSet>>;
fn get_exercise_lesson(&self, exercise_id: Ustr) -> Option<Ustr>;
fn get_dependencies(&self, unit_id: Ustr) -> Option<Arc<UstrSet>>;
fn get_dependents(&self, unit_id: Ustr) -> Option<Arc<UstrSet>>;
fn get_dependency_sinks(&self) -> Arc<UstrSet>;
fn get_encompasses(&self, unit_id: Ustr) -> Option<Vec<(Ustr, f32)>>;
fn get_encompassed_by(&self, unit_id: Ustr) -> Option<Vec<(Ustr, f32)>>;
fn get_supersedes(&self, unit_id: Ustr) -> Option<Arc<UstrSet>>;
fn get_superseded_by(&self, unit_id: Ustr) -> Option<Arc<UstrSet>>;
fn check_cycles(&self) -> Result<(), UnitGraphError>;
fn generate_dot_graph(&self, courses_only: bool) -> String;
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct InMemoryUnitGraph {
type_map: UstrMap<UnitType>,
course_lesson_map: UstrMap<Arc<UstrSet>>,
starting_lessons_map: UstrMap<Arc<UstrSet>>,
lesson_course_map: UstrMap<Ustr>,
lesson_exercise_map: UstrMap<Arc<UstrSet>>,
exercise_lesson_map: UstrMap<Ustr>,
dependency_graph: UstrMap<Arc<UstrSet>>,
dependent_graph: UstrMap<Arc<UstrSet>>,
dependency_sinks: Arc<UstrSet>,
encompasses_graph: UstrMap<Vec<(Ustr, f32)>>,
encompassed_by: UstrMap<Vec<(Ustr, f32)>>,
supersedes_graph: UstrMap<Arc<UstrSet>>,
superseded_by: UstrMap<Arc<UstrSet>>,
}
impl InMemoryUnitGraph {
fn update_dependency_sinks(&mut self, unit_id: Ustr, dependencies: &[Ustr]) {
let empty = Arc::new(UstrSet::default());
let current_dependencies = self.dependency_graph.get(&unit_id).unwrap_or(&empty);
if current_dependencies.is_empty() && dependencies.is_empty() {
Arc::make_mut(&mut self.dependency_sinks).insert(unit_id);
} else {
Arc::make_mut(&mut self.dependency_sinks).remove(&unit_id);
}
if self.get_lesson_course(unit_id).is_some() {
Arc::make_mut(&mut self.dependency_sinks).remove(&unit_id);
}
for dependency_id in dependencies {
self.update_dependency_sinks(*dependency_id, &[]);
}
}
fn update_unit_type(&mut self, unit_id: Ustr, unit_type: UnitType) -> Result<()> {
match self.type_map.get(&unit_id) {
None => {
self.type_map.insert(unit_id, unit_type);
Ok(())
}
Some(existing_type) => {
if unit_type == *existing_type {
Ok(())
} else {
Err(anyhow!(
"cannot update unit type of unit {unit_id} from type {existing_type:#?}) \
to {unit_type:#?}.",
))
}
}
}
}
fn add_course_helper(&mut self, course_id: Ustr) -> Result<()> {
ensure!(
!self.type_map.contains_key(&course_id),
"course with ID {course_id} already exists",
);
self.update_unit_type(course_id, UnitType::Course)?;
Ok(())
}
fn add_lesson_helper(&mut self, lesson_id: Ustr, course_id: Ustr) -> Result<()> {
ensure!(
!self.type_map.contains_key(&lesson_id),
"lesson with ID {lesson_id} already exists",
);
self.update_unit_type(lesson_id, UnitType::Lesson)?;
self.update_unit_type(course_id, UnitType::Course)?;
self.lesson_course_map.insert(lesson_id, course_id);
Arc::make_mut(self.course_lesson_map.entry(course_id).or_default()).insert(lesson_id);
Ok(())
}
fn add_exercise_helper(&mut self, exercise_id: Ustr, lesson_id: Ustr) -> Result<()> {
ensure!(
!self.type_map.contains_key(&exercise_id),
"exercise with ID {exercise_id} already exists",
);
self.update_unit_type(exercise_id, UnitType::Exercise)?;
self.update_unit_type(lesson_id, UnitType::Lesson)?;
Arc::make_mut(self.lesson_exercise_map.entry(lesson_id).or_default()).insert(exercise_id);
self.exercise_lesson_map.insert(exercise_id, lesson_id);
Ok(())
}
#[cfg_attr(coverage, coverage(off))]
fn verify_dependencies(
&self,
unit_id: Ustr,
unit_type: &UnitType,
dependencies: &[Ustr],
) -> Result<()> {
ensure!(
*unit_type != UnitType::Exercise,
"exercise {unit_id} cannot have dependencies",
);
ensure!(
dependencies.iter().all(|dep| *dep != unit_id),
"unit {unit_id} cannot depend on itself",
);
ensure!(
self.type_map.contains_key(&unit_id),
"unit {unit_id} of type {unit_type:?} must be explicitly added before adding \
dependencies",
);
Ok(())
}
fn add_dependencies_helper(
&mut self,
unit_id: Ustr,
unit_type: &UnitType,
dependencies: &[Ustr],
) -> Result<()> {
self.verify_dependencies(unit_id, unit_type, dependencies)?;
self.update_dependency_sinks(unit_id, dependencies);
Arc::make_mut(self.dependency_graph.entry(unit_id).or_default()).extend(dependencies);
for dependency_id in dependencies {
Arc::make_mut(self.dependent_graph.entry(*dependency_id).or_default()).insert(unit_id);
}
Ok(())
}
fn add_encompassed_helper(
&mut self,
unit_id: Ustr,
dependencies: &[Ustr],
encompassed: &[(Ustr, f32)],
) -> Result<()> {
ensure!(
encompassed
.iter()
.all(|(_, weight)| (0.0..=1.0).contains(weight)),
"encompassed units of unit {unit_id} must have weights within the range [0.0, 1.0]",
);
let mut full_encompassed = encompassed.to_vec();
for dependency in dependencies {
if !encompassed
.iter()
.any(|(encompassed_id, _)| *encompassed_id == *dependency)
{
full_encompassed.push((*dependency, 1.0));
}
}
self.encompasses_graph
.entry(unit_id)
.or_default()
.extend(full_encompassed.clone());
for (encompassed_id, weight) in full_encompassed {
self.encompassed_by
.entry(encompassed_id)
.or_default()
.push((unit_id, weight));
}
Ok(())
}
fn check_graph_cycles(
graph_keys: &[Ustr],
get_neighbors: impl Fn(Ustr) -> Option<Vec<Ustr>>,
check_reverse: impl Fn(Ustr, Ustr) -> Result<()>,
cycle_msg: &str,
) -> Result<()> {
let mut visited = UstrSet::default();
for unit_id in graph_keys {
if visited.contains(unit_id) {
continue;
}
let mut stack: Vec<Vec<Ustr>> = vec![vec![*unit_id]];
while let Some(path) = stack.pop() {
let current_id = *path.last().unwrap_or(&Ustr::default());
if visited.contains(¤t_id) {
continue;
}
visited.insert(current_id);
if let Some(neighbors) = get_neighbors(current_id) {
for neighbor_id in neighbors {
check_reverse(current_id, neighbor_id)?;
if path.contains(&neighbor_id) {
let mut cycle_path = path.clone();
cycle_path.push(neighbor_id);
let path_str = cycle_path.iter().fold(String::new(), |mut s, id| {
if !s.is_empty() {
s.push_str(" -> ");
}
let _ = write!(s, "{id}");
s
});
bail!("{cycle_msg}: {path_str}");
}
let mut new_path = path.clone();
new_path.push(neighbor_id);
stack.push(new_path);
}
}
}
}
Ok(())
}
fn check_cycles_helper(&self) -> Result<()> {
let dep_keys: Vec<Ustr> = self.dependency_graph.keys().copied().collect();
Self::check_graph_cycles(
&dep_keys,
|id| {
self.get_dependencies(id)
.map(|s| s.iter().copied().collect())
},
|current, dep| {
let dependents = self.get_dependents(dep).unwrap_or_default();
ensure!(
dependents.contains(¤t),
"unit {current} lists unit {dep} as a dependency but the dependent \
relationship does not exist",
);
Ok(())
},
"cycle in dependency graph detected",
)?;
let sup_keys: Vec<Ustr> = self.supersedes_graph.keys().copied().collect();
Self::check_graph_cycles(
&sup_keys,
|id| self.get_supersedes(id).map(|s| s.iter().copied().collect()),
|current, sup| {
let superseding = self.get_superseded_by(sup).unwrap_or_default();
ensure!(
superseding.contains(¤t),
"unit {current} lists unit {sup} as a superseded unit but the superseding \
relationship does not exist",
);
Ok(())
},
"cycle in superseded graph detected",
)?;
let enc_keys: Vec<Ustr> = self.encompasses_graph.keys().copied().collect();
Self::check_graph_cycles(
&enc_keys,
|id| {
self.get_encompasses(id)
.map(|v| v.iter().map(|(id, _)| *id).collect())
},
|current, enc| {
let encompassing = self.get_encompassed_by(enc).unwrap_or_default();
ensure!(
encompassing.iter().any(|(u, _)| *u == current),
"unit {current} lists unit {enc} as an encompassed unit but the encompassing \
relationship does not exist",
);
Ok(())
},
"cycle in encompassed graph detected",
)?;
Ok(())
}
}
impl UnitGraph for InMemoryUnitGraph {
fn add_course(&mut self, course_id: Ustr) -> Result<(), UnitGraphError> {
self.add_course_helper(course_id)
.map_err(|e| UnitGraphError::AddUnit(course_id, UnitType::Course, e))
}
fn add_lesson(&mut self, lesson_id: Ustr, course_id: Ustr) -> Result<(), UnitGraphError> {
self.add_lesson_helper(lesson_id, course_id)
.map_err(|e| UnitGraphError::AddUnit(lesson_id, UnitType::Lesson, e))
}
fn add_exercise(&mut self, exercise_id: Ustr, lesson_id: Ustr) -> Result<(), UnitGraphError> {
self.add_exercise_helper(exercise_id, lesson_id)
.map_err(|e| UnitGraphError::AddUnit(exercise_id, UnitType::Exercise, e))
}
fn add_dependencies(
&mut self,
unit_id: Ustr,
unit_type: UnitType,
dependencies: &[Ustr],
) -> Result<(), UnitGraphError> {
self.add_dependencies_helper(unit_id, &unit_type, dependencies)
.map_err(|e| UnitGraphError::AddDependencies(unit_id, unit_type, e))
}
fn add_encompassed(
&mut self,
unit_id: Ustr,
dependencies: &[Ustr],
encompassed: &[(Ustr, f32)],
) -> Result<(), UnitGraphError> {
self.add_encompassed_helper(unit_id, dependencies, encompassed)
.map_err(|e| UnitGraphError::AddEncompassed(unit_id, e))
}
fn set_encompasing_equals_dependency(&mut self) {
self.encompasses_graph.clear();
self.encompassed_by.clear();
}
fn encompasing_equals_dependency(&self) -> bool {
self.encompasses_graph.is_empty() && self.encompassed_by.is_empty()
}
fn add_superseded(&mut self, unit_id: Ustr, superseded: &[Ustr]) {
if superseded.is_empty() {
return;
}
Arc::make_mut(self.supersedes_graph.entry(unit_id).or_default()).extend(superseded);
for superseded_id in superseded {
Arc::make_mut(self.superseded_by.entry(*superseded_id).or_default()).insert(unit_id);
}
}
fn get_unit_type(&self, unit_id: Ustr) -> Option<UnitType> {
self.type_map.get(&unit_id).cloned()
}
fn get_course_lessons(&self, course_id: Ustr) -> Option<Arc<UstrSet>> {
self.course_lesson_map.get(&course_id).cloned()
}
fn get_starting_lessons(&self, course_id: Ustr) -> Option<Arc<UstrSet>> {
self.starting_lessons_map.get(&course_id).cloned()
}
fn update_starting_lessons(&mut self) {
let empty = Arc::new(UstrSet::default());
for course_id in self.course_lesson_map.keys() {
let lessons = self.course_lesson_map.get(course_id).unwrap_or(&empty);
let starting_lessons: UstrSet = lessons
.iter()
.copied()
.filter(|lesson_id| {
let dependencies = self.get_dependencies(*lesson_id);
match dependencies {
None => true,
Some(dependencies) => lessons.is_disjoint(&dependencies),
}
})
.collect();
if self.dependency_sinks.contains(course_id) {
let has_starting_dependencies = starting_lessons.iter().any(|lesson_id| {
self.get_dependencies(*lesson_id)
.is_some_and(|dependencies| {
!dependencies.is_empty()
&& dependencies
.iter()
.all(|dep| self.get_unit_type(*dep).is_some())
})
});
if has_starting_dependencies {
Arc::make_mut(&mut self.dependency_sinks).remove(course_id);
}
}
self.starting_lessons_map
.insert(*course_id, Arc::new(starting_lessons));
}
}
fn get_lesson_course(&self, lesson_id: Ustr) -> Option<Ustr> {
self.lesson_course_map.get(&lesson_id).copied()
}
fn get_lesson_exercises(&self, lesson_id: Ustr) -> Option<Arc<UstrSet>> {
self.lesson_exercise_map.get(&lesson_id).cloned()
}
fn get_exercise_lesson(&self, exercise_id: Ustr) -> Option<Ustr> {
self.exercise_lesson_map.get(&exercise_id).copied()
}
fn get_dependencies(&self, unit_id: Ustr) -> Option<Arc<UstrSet>> {
self.dependency_graph.get(&unit_id).cloned()
}
fn get_dependents(&self, unit_id: Ustr) -> Option<Arc<UstrSet>> {
self.dependent_graph.get(&unit_id).cloned()
}
fn get_dependency_sinks(&self) -> Arc<UstrSet> {
self.dependency_sinks.clone()
}
fn get_encompasses(&self, unit_id: Ustr) -> Option<Vec<(Ustr, f32)>> {
if self.encompasses_graph.is_empty() {
self.get_dependencies(unit_id)
.map(|dependencies| dependencies.iter().copied().map(|dep| (dep, 1.0)).collect())
} else {
self.encompasses_graph.get(&unit_id).cloned()
}
}
fn get_encompassed_by(&self, unit_id: Ustr) -> Option<Vec<(Ustr, f32)>> {
if self.encompassed_by.is_empty() {
self.get_dependents(unit_id)
.map(|dependents| dependents.iter().copied().map(|dep| (dep, 1.0)).collect())
} else {
self.encompassed_by.get(&unit_id).cloned()
}
}
fn get_supersedes(&self, unit_id: Ustr) -> Option<Arc<UstrSet>> {
self.supersedes_graph.get(&unit_id).cloned()
}
fn get_superseded_by(&self, unit_id: Ustr) -> Option<Arc<UstrSet>> {
self.superseded_by.get(&unit_id).cloned()
}
fn check_cycles(&self) -> Result<(), UnitGraphError> {
self.check_cycles_helper()
.map_err(UnitGraphError::CheckCycles)
}
fn generate_dot_graph(&self, courses_only: bool) -> String {
let mut output = String::from("digraph dependent_graph {\n");
let mut courses = self.course_lesson_map.keys().copied().collect::<Vec<_>>();
courses.sort();
for course_id in courses {
let _ = writeln!(output, " \"{course_id}\" [color=red, style=filled]");
let mut dependents = self
.get_dependents(course_id)
.unwrap_or_default()
.iter()
.copied()
.collect::<Vec<_>>();
dependents.retain(|dependent_id| {
if courses_only {
self.get_unit_type(*dependent_id) == Some(UnitType::Course)
} else {
true
}
});
if !courses_only {
dependents.extend(
self.get_starting_lessons(course_id)
.unwrap_or_default()
.iter(),
);
}
dependents.sort();
for dependent in dependents {
let _ = writeln!(output, " \"{course_id}\" -> \"{dependent}\"");
}
if courses_only {
continue;
}
let mut lessons = self
.get_course_lessons(course_id)
.unwrap_or_default()
.iter()
.copied()
.collect::<Vec<_>>();
lessons.sort();
for lesson_id in lessons {
let _ = writeln!(output, " \"{lesson_id}\" [color=blue, style=filled]");
let mut dependents = self
.get_dependents(lesson_id)
.unwrap_or_default()
.iter()
.copied()
.collect::<Vec<_>>();
dependents.sort();
for dependent in dependents {
let _ = writeln!(output, " \"{lesson_id}\" -> \"{dependent}\"");
}
}
}
output.push_str("}\n");
output
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod test {
use anyhow::Result;
use indoc::indoc;
use std::sync::Arc;
use ustr::{Ustr, UstrSet};
use crate::{
data::UnitType,
graph::{InMemoryUnitGraph, UnitGraph},
};
#[test]
fn get_unit_type() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let id = Ustr::from("id1");
graph.add_course(id)?;
graph.add_dependencies(id, UnitType::Course, &[])?;
assert_eq!(graph.get_unit_type(id), Some(UnitType::Course));
Ok(())
}
#[test]
fn get_course_lessons_and_exercises() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let course_id = Ustr::from("course1");
let lesson1_id = Ustr::from("course1::lesson1");
let lesson2_id = Ustr::from("course1::lesson2");
let lesson1_exercise1_id = Ustr::from("course1::lesson1::exercise1");
let lesson1_exercise2_id = Ustr::from("course1::lesson1::exercise2");
let lesson2_exercise1_id = Ustr::from("course1::lesson2::exercise1");
let lesson2_exercise2_id = Ustr::from("course1::lesson2::exercise2");
graph.add_course(course_id)?;
graph.add_dependencies(course_id, UnitType::Course, &[])?;
graph.add_lesson(lesson1_id, course_id)?;
graph.add_exercise(lesson1_exercise1_id, lesson1_id)?;
graph.add_exercise(lesson1_exercise2_id, lesson1_id)?;
graph.add_lesson(lesson2_id, course_id)?;
graph.add_exercise(lesson2_exercise1_id, lesson2_id)?;
graph.add_exercise(lesson2_exercise2_id, lesson2_id)?;
let course_lessons = graph.get_course_lessons(course_id).unwrap();
assert_eq!(course_lessons.len(), 2);
assert!(course_lessons.contains(&lesson1_id));
assert!(course_lessons.contains(&lesson2_id));
let lesson1_exercises = graph.get_lesson_exercises(lesson1_id).unwrap();
assert_eq!(lesson1_exercises.len(), 2);
assert!(lesson1_exercises.contains(&lesson1_exercise1_id));
assert!(lesson1_exercises.contains(&lesson1_exercise2_id));
assert_eq!(
graph.get_exercise_lesson(lesson1_exercise1_id).unwrap(),
lesson1_id
);
assert_eq!(
graph.get_exercise_lesson(lesson1_exercise2_id).unwrap(),
lesson1_id
);
let lesson2_exercises = graph.get_lesson_exercises(lesson2_id).unwrap();
assert_eq!(lesson2_exercises.len(), 2);
assert!(lesson2_exercises.contains(&lesson2_exercise1_id));
assert!(lesson2_exercises.contains(&lesson2_exercise2_id));
assert_eq!(
graph.get_exercise_lesson(lesson2_exercise1_id).unwrap(),
lesson2_id
);
assert_eq!(
graph.get_exercise_lesson(lesson2_exercise2_id).unwrap(),
lesson2_id
);
Ok(())
}
#[test]
fn dependency_graph() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let course1_id = Ustr::from("course1");
let course2_id = Ustr::from("course2");
let course3_id = Ustr::from("course3");
let course4_id = Ustr::from("course4");
let course5_id = Ustr::from("course5");
graph.add_course(course1_id)?;
graph.add_course(course2_id)?;
graph.add_course(course3_id)?;
graph.add_course(course4_id)?;
graph.add_course(course5_id)?;
graph.add_dependencies(course1_id, UnitType::Course, &[])?;
graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
graph.add_dependencies(course3_id, UnitType::Course, &[course1_id])?;
graph.add_dependencies(course4_id, UnitType::Course, &[course2_id])?;
graph.add_dependencies(course5_id, UnitType::Course, &[course3_id])?;
{
let dependents = graph.get_dependents(course1_id).unwrap();
assert_eq!(dependents.len(), 2);
assert!(dependents.contains(&course2_id));
assert!(dependents.contains(&course3_id));
assert!(graph.get_dependencies(course1_id).unwrap().is_empty());
}
{
let dependents = graph.get_dependents(course2_id).unwrap();
assert_eq!(dependents.len(), 1);
assert!(dependents.contains(&course4_id));
let dependencies = graph.get_dependencies(course2_id).unwrap();
assert_eq!(dependencies.len(), 1);
assert!(dependencies.contains(&course1_id));
}
{
let dependents = graph.get_dependents(course3_id).unwrap();
assert_eq!(dependents.len(), 1);
assert!(dependents.contains(&course5_id));
let dependencies = graph.get_dependencies(course3_id).unwrap();
assert_eq!(dependencies.len(), 1);
assert!(dependencies.contains(&course1_id));
}
{
assert!(graph.get_dependents(course4_id).is_none());
let dependencies = graph.get_dependencies(course4_id).unwrap();
assert_eq!(dependencies.len(), 1);
assert!(dependencies.contains(&course2_id));
}
{
assert!(graph.get_dependents(course5_id).is_none());
let dependencies = graph.get_dependencies(course5_id).unwrap();
assert_eq!(dependencies.len(), 1);
assert!(dependencies.contains(&course3_id));
}
let sinks = graph.get_dependency_sinks();
assert_eq!(sinks.len(), 1);
assert!(sinks.contains(&course1_id));
graph.check_cycles()?;
Ok(())
}
#[test]
fn courses_with_starting_dependencies_not_in_sinks() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let course1_id = Ustr::from("course1");
let course2_id = Ustr::from("course2");
let course2_lesson_1_id = Ustr::from("course2::lesson1");
graph.add_course(course1_id)?;
graph.add_course(course2_id)?;
graph.add_lesson(course2_lesson_1_id, course2_id)?;
graph.add_dependencies(course1_id, UnitType::Course, &[])?;
graph.add_dependencies(course2_id, UnitType::Course, &[])?;
graph.add_dependencies(course2_lesson_1_id, UnitType::Lesson, &[course1_id])?;
graph.update_starting_lessons();
let sinks = graph.get_dependency_sinks();
assert_eq!(*sinks, vec![course1_id].into_iter().collect::<UstrSet>());
Ok(())
}
#[test]
fn encompassing_graph() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let course1_id = Ustr::from("course1");
let course2_id = Ustr::from("course2");
let course3_id = Ustr::from("course3");
graph.add_course(course1_id)?;
graph.add_course(course2_id)?;
graph.add_course(course3_id)?;
graph.add_dependencies(course1_id, UnitType::Course, &[])?;
graph.add_encompassed(course1_id, &[], &[])?;
graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
graph.add_encompassed(course2_id, &[course1_id], &[])?;
graph.add_dependencies(course3_id, UnitType::Course, &[course1_id])?;
graph.add_encompassed(
course3_id,
&[course1_id],
&[(course1_id, 0.5), (course2_id, 0.5)],
)?;
assert!(!graph.encompasing_equals_dependency());
{
let encompassed = graph.get_encompasses(course1_id).unwrap();
assert_eq!(encompassed.len(), 0);
let encompassed_by = graph.get_encompassed_by(course1_id).unwrap();
assert_eq!(encompassed_by.len(), 2);
assert!(encompassed_by.contains(&(course3_id, 0.5)));
assert!(encompassed_by.contains(&(course2_id, 1.0)));
}
{
let encompassed = graph.get_encompasses(course3_id).unwrap();
assert_eq!(encompassed.len(), 2);
assert!(encompassed.contains(&(course1_id, 0.5)));
assert!(encompassed.contains(&(course2_id, 0.5)));
let encompassed_by = graph.get_encompassed_by(course2_id).unwrap();
assert_eq!(encompassed_by.len(), 1);
assert!(encompassed_by.contains(&(course3_id, 0.5)));
}
{
let encompassed = graph.get_encompasses(course2_id).unwrap();
assert_eq!(encompassed.len(), 1);
assert!(encompassed.contains(&(course1_id, 1.0)));
let encompassed_by = graph.get_encompassed_by(course3_id);
assert!(encompassed_by.is_none());
}
Ok(())
}
#[test]
fn encompassing_equals_dependencies() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let course1_id = Ustr::from("course1");
let course2_id = Ustr::from("course2");
let course3_id = Ustr::from("course3");
graph.add_course(course1_id)?;
graph.add_course(course2_id)?;
graph.add_course(course3_id)?;
graph.add_dependencies(course1_id, UnitType::Course, &[])?;
graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
graph.add_dependencies(course3_id, UnitType::Course, &[course1_id])?;
assert!(graph.encompasing_equals_dependency());
{
let encompassed = graph.get_encompasses(course1_id).unwrap();
assert_eq!(encompassed.len(), 0);
let dependencies = graph.get_dependencies(course1_id).unwrap();
assert_eq!(dependencies.len(), 0);
let encompassed_by = graph.get_encompassed_by(course1_id).unwrap();
assert_eq!(encompassed_by.len(), 2);
assert!(encompassed_by.contains(&(course2_id, 1.0)));
assert!(encompassed_by.contains(&(course3_id, 1.0)));
let dependents = graph.get_dependents(course1_id).unwrap();
assert_eq!(dependents.len(), 2);
assert!(dependents.contains(&course2_id));
assert!(dependents.contains(&course3_id));
}
{
let encompassed = graph.get_encompasses(course2_id).unwrap();
assert_eq!(encompassed.len(), 1);
assert!(encompassed.contains(&(course1_id, 1.0)));
let dependencies = graph.get_dependencies(course2_id).unwrap();
assert_eq!(dependencies.len(), 1);
assert!(dependencies.contains(&course1_id));
let encompassed_by = graph.get_encompassed_by(course2_id);
assert!(encompassed_by.is_none());
let dependents = graph.get_dependents(course2_id);
assert!(dependents.is_none());
}
{
let encompassed = graph.get_encompasses(course3_id).unwrap();
assert_eq!(encompassed.len(), 1);
assert!(encompassed.contains(&(course1_id, 1.0)));
let dependencies = graph.get_dependencies(course3_id).unwrap();
assert_eq!(dependencies.len(), 1);
assert!(dependencies.contains(&course1_id));
let encompassed_by = graph.get_encompassed_by(course3_id);
assert!(encompassed_by.is_none());
let dependents = graph.get_dependents(course3_id);
assert!(dependents.is_none());
}
Ok(())
}
#[test]
fn superseding_graph() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let course1_id = Ustr::from("course1");
let course2_id = Ustr::from("course2");
let course3_id = Ustr::from("course3");
graph.add_course(course1_id)?;
graph.add_course(course2_id)?;
graph.add_course(course3_id)?;
graph.add_dependencies(course1_id, UnitType::Course, &[])?;
graph.add_superseded(course1_id, &[course2_id]);
graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
graph.add_superseded(course2_id, &[course3_id]);
graph.add_dependencies(course3_id, UnitType::Course, &[course2_id])?;
{
let superseded = graph.get_supersedes(course1_id).unwrap();
assert_eq!(superseded.len(), 1);
assert!(superseded.contains(&course2_id));
assert!(graph.get_superseded_by(course1_id).is_none());
}
{
let superseded = graph.get_supersedes(course2_id).unwrap();
assert_eq!(superseded.len(), 1);
assert!(superseded.contains(&course3_id));
let superseded_by = graph.get_superseded_by(course2_id).unwrap();
assert_eq!(superseded_by.len(), 1);
assert!(superseded_by.contains(&course1_id));
}
{
assert!(graph.get_supersedes(course3_id).is_none());
let superseded_by = graph.get_superseded_by(course3_id).unwrap();
assert_eq!(superseded_by.len(), 1);
assert!(superseded_by.contains(&course2_id));
}
Ok(())
}
#[test]
fn generate_dot_graph() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let course1_id = Ustr::from("1");
let course1_lesson1_id = Ustr::from("1::1");
let course1_lesson2_id = Ustr::from("1::2");
let course2_id = Ustr::from("2");
let course2_lesson1_id = Ustr::from("2::1");
let course3_id = Ustr::from("3");
let course3_lesson1_id = Ustr::from("3::1");
let course3_lesson2_id = Ustr::from("3::2");
graph.add_lesson(course1_lesson1_id, course1_id)?;
graph.add_lesson(course1_lesson2_id, course1_id)?;
graph.add_lesson(course2_lesson1_id, course2_id)?;
graph.add_lesson(course3_lesson1_id, course3_id)?;
graph.add_lesson(course3_lesson2_id, course3_id)?;
graph.add_dependencies(course1_id, UnitType::Course, &[])?;
graph.add_dependencies(course1_lesson2_id, UnitType::Lesson, &[course1_lesson1_id])?;
graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
graph.add_dependencies(course3_id, UnitType::Course, &[course2_id])?;
graph.add_dependencies(course3_lesson2_id, UnitType::Lesson, &[course3_lesson1_id])?;
graph.update_starting_lessons();
let dot = graph.generate_dot_graph(false);
let expected = indoc! {r#"
digraph dependent_graph {
"1" [color=red, style=filled]
"1" -> "1::1"
"1" -> "2"
"1::1" [color=blue, style=filled]
"1::1" -> "1::2"
"1::2" [color=blue, style=filled]
"2" [color=red, style=filled]
"2" -> "2::1"
"2" -> "3"
"2::1" [color=blue, style=filled]
"3" [color=red, style=filled]
"3" -> "3::1"
"3::1" [color=blue, style=filled]
"3::1" -> "3::2"
"3::2" [color=blue, style=filled]
}
"#};
assert_eq!(dot, expected);
let dot_courses_only = graph.generate_dot_graph(true);
let expected_courses_only = indoc! {r#"
digraph dependent_graph {
"1" [color=red, style=filled]
"1" -> "2"
"2" [color=red, style=filled]
"2" -> "3"
"3" [color=red, style=filled]
}
"#};
assert_eq!(dot_courses_only, expected_courses_only);
Ok(())
}
#[test]
fn duplicate_ids() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let course_id = Ustr::from("course_id");
graph.add_course(course_id)?;
let _ = graph.add_course(course_id).is_err();
let lesson_id = Ustr::from("lesson_id");
graph.add_lesson(lesson_id, course_id)?;
let _ = graph.add_lesson(lesson_id, course_id).is_err();
let exercise_id = Ustr::from("exercise_id");
graph.add_exercise(exercise_id, lesson_id)?;
let _ = graph.add_exercise(exercise_id, lesson_id).is_err();
Ok(())
}
#[test]
fn update_unit_type_different_types() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let unit_id = Ustr::from("unit_id");
graph.update_unit_type(unit_id, UnitType::Course)?;
assert!(graph.update_unit_type(unit_id, UnitType::Lesson).is_err());
Ok(())
}
#[test]
fn dependencies_cycle() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let course1_id = Ustr::from("course1");
let course2_id = Ustr::from("course2");
let course3_id = Ustr::from("course3");
let course4_id = Ustr::from("course4");
let course5_id = Ustr::from("course5");
graph.add_course(course1_id)?;
graph.add_course(course2_id)?;
graph.add_course(course3_id)?;
graph.add_course(course4_id)?;
graph.add_course(course5_id)?;
graph.add_dependencies(course1_id, UnitType::Course, &[])?;
graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
graph.add_dependencies(course3_id, UnitType::Course, &[course1_id])?;
graph.add_dependencies(course4_id, UnitType::Course, &[course2_id])?;
graph.add_dependencies(course5_id, UnitType::Course, &[course3_id])?;
graph.add_dependencies(course1_id, UnitType::Course, &[course5_id])?;
assert!(graph.check_cycles().is_err());
Ok(())
}
#[test]
fn encompassed_cycle() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let course1_id = Ustr::from("course1");
let course2_id = Ustr::from("course2");
let course3_id = Ustr::from("course3");
let course4_id = Ustr::from("course4");
let course5_id = Ustr::from("course5");
graph.add_encompassed(course2_id, &[course1_id], &[])?;
graph.add_encompassed(course3_id, &[course1_id], &[])?;
graph.add_encompassed(course4_id, &[course2_id], &[])?;
graph.add_encompassed(course5_id, &[course3_id], &[])?;
graph.add_encompassed(course1_id, &[course5_id], &[])?;
assert!(graph.check_cycles().is_err());
Ok(())
}
#[test]
fn superseded_cycle() {
let mut graph = InMemoryUnitGraph::default();
let course1_id = Ustr::from("course1");
let course2_id = Ustr::from("course2");
let course3_id = Ustr::from("course3");
let course4_id = Ustr::from("course4");
let course5_id = Ustr::from("course5");
graph.add_superseded(course2_id, &[course1_id]);
graph.add_superseded(course3_id, &[course1_id]);
graph.add_superseded(course4_id, &[course2_id]);
graph.add_superseded(course5_id, &[course3_id]);
graph.add_superseded(course1_id, &[course5_id]);
assert!(graph.check_cycles().is_err());
}
#[test]
fn missing_dependent_relationship() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let course_id = Ustr::from("course_id");
let lesson1_id = Ustr::from("lesson1_id");
let lesson2_id = Ustr::from("lesson2_id");
graph.add_course(course_id).unwrap();
graph.add_lesson(lesson1_id, course_id).unwrap();
graph.add_lesson(lesson2_id, course_id).unwrap();
graph.add_dependencies(lesson2_id, UnitType::Lesson, &[lesson1_id])?;
graph
.dependent_graph
.insert(lesson1_id, Arc::new(UstrSet::default()));
assert!(graph.check_cycles().is_err());
graph.dependency_graph.remove(&lesson1_id);
assert!(graph.check_cycles().is_err());
Ok(())
}
#[test]
fn missing_encompasing_relationship() -> Result<()> {
let mut graph = InMemoryUnitGraph::default();
let lesson1_id = Ustr::from("lesson1_id");
let lesson2_id = Ustr::from("lesson2_id");
graph.add_encompassed(lesson2_id, &[lesson1_id], &[])?;
graph.encompassed_by.insert(lesson1_id, Vec::default());
assert!(graph.check_cycles().is_err());
graph.encompassed_by.remove(&lesson1_id);
assert!(graph.check_cycles().is_err());
Ok(())
}
#[test]
fn missing_superseding_relationship() {
let mut graph = InMemoryUnitGraph::default();
let lesson1_id = Ustr::from("lesson1_id");
let lesson2_id = Ustr::from("lesson2_id");
graph.add_superseded(lesson2_id, &[lesson1_id]);
graph
.superseded_by
.insert(lesson1_id, Arc::new(UstrSet::default()));
assert!(graph.check_cycles().is_err());
graph.dependency_graph.remove(&lesson1_id);
assert!(graph.check_cycles().is_err());
}
}