Skip to main content

trane/
graph.rs

1//! Defines the dependency graph of units of knowledge, their dependency relationships, and basic
2//! read and write operations.
3//!
4//! The dependency graph is perhaps the most important part of the design of Trane so its nature and
5//! purpose should be well documented. At its core, the goal of Trane is to guide students through
6//! the graph of units of knowledge composed of exercises, by having each successive unit teach a
7//! skill that can be acquired once the previous units are sufficiently mastered. This process of
8//! repetition of mastered exercises and introduction of new ones should lead to the complete
9//! mastery of complex meta-skills such as jazz improvisation, chess, piano, etc. that are in fact
10//! the mastered integration of many smaller and interlinked skills.
11//!
12//! This graph is implemented by simulating a directed acyclic graph (DAG) of units and
13//! dependency/dependents relationships among them. A unit can be of three types:
14//!
15//! 1. An exercise, which represents a single task testing a skill which the student is required to
16//!    assess when practiced.
17//! 2. A lesson, which represents a collection of exercises which test the same skill and can be
18//!    practiced in any order.
19//! 3. A course, a collection of lessons which are related. It mostly exists to help organize the
20//!    material in larger entities which share some context.
21//!
22//! The relationships between the units is one of the following:
23//!
24//! 1. A course or lesson A is a dependency of course or lesson B if A needs to be sufficiently
25//!    mastered before B can be practiced.
26//! 2. The reverse relationship. Thus, we say that B is a dependent of A.
27//! 3. A course or lesson A is encompassed by another course or lesson B if doing well in the
28//!    exercises of B implies that the skills or knowledge tested by the exercises of A is being
29//!    used. This relationship is used by the scheduler to propagate rewards and to filter exercises
30//!    that are highly encompassed by others during scheduling.
31//! 4. The reverse relationship. Thus, we say that B encompasses A.
32//! 5. A course or lesson A is superseded by another course or lesson B if sufficient mastery of B
33//!    makes showing exercises from A redundant.
34//! 6. The reverse relationship. Thus, we say that B supersedes A.
35//!
36//! The graph also provides a number of operations to manipulate the graph, which are only used when
37//! reading the Trane library (see [`course_library`](crate::course_library)), and another few to
38//! derive information from the graph ("which are the lessons in a course?" for example). The graph
39//! is not in any way responsible for how the exercises are scheduled (see
40//! [`scheduler`](crate::scheduler)) nor it stores any information about a student's practice (see
41//! [`practice_stats`](crate::practice_stats)) or preferences (see [`blacklist`](crate::blacklist),
42//! [`filter_manager`](crate::filter_manager) and [`review_list`](crate::review_list)).
43
44use 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
52/// Stores the units and their dependency relationships (for lessons and courses only, since
53/// exercises do not define any dependencies). It provides basic functions to update the graph and
54/// retrieve information about it for use during scheduling and student's requests.
55///
56/// The operations that update the graph are only used when reading the Trane library during
57/// startup. A user that copies new courses to an existing and currently opened library will need to
58/// restart Trane to see the changes take effect.
59pub trait UnitGraph {
60    /// Adds a new course to the unit graph.
61    fn add_course(&mut self, course_id: Ustr) -> Result<(), UnitGraphError>;
62
63    /// Adds a new lesson to the unit graph. It also takes the ID of the course to which this lesson
64    /// belongs.
65    fn add_lesson(&mut self, lesson_id: Ustr, course_id: Ustr) -> Result<(), UnitGraphError>;
66
67    /// Adds a new exercise to the unit graph. It also takes the ID of the lesson to which this
68    /// exercise belongs.
69    fn add_exercise(&mut self, exercise_id: Ustr, lesson_id: Ustr) -> Result<(), UnitGraphError>;
70
71    /// Takes a unit and its dependencies and updates the graph accordingly. Returns an error if
72    /// `unit_type` is `UnitType::Exercise` as only courses and lessons are allowed to have
73    /// dependencies. An error is also returned if the unit was not previously added by calling one
74    /// of `add_course` or `add_lesson`.
75    fn add_dependencies(
76        &mut self,
77        unit_id: Ustr,
78        unit_type: UnitType,
79        dependencies: &[Ustr],
80    ) -> Result<(), UnitGraphError>;
81
82    /// Adds the list of encompassed units for the given unit to the graph. Dependencies not in the
83    /// list of encompassed units are added with a default weight of 1.0. Returns an error if any
84    /// of the weights are not within the range [0.0, 1.0].
85    fn add_encompassed(
86        &mut self,
87        unit_id: Ustr,
88        dependencies: &[Ustr],
89        encompassed: &[(Ustr, f32)],
90    ) -> Result<(), UnitGraphError>;
91
92    /// Tells `UnitGraph` that the encompassing and dependency graphs are the same. That is, no
93    /// manifest explicitly declared encompassed units. In this case, the encompassing graph is
94    /// identical to the dependency graph with all weights set to 1.0. The caller should use this
95    /// function after building the full graph to avoid the overhead of storing two identical
96    /// graphs.
97    fn set_encompasing_equals_dependency(&mut self);
98
99    /// Whether the encompassing and dependency graphs are effectively the same.
100    fn encompasing_equals_dependency(&self) -> bool;
101
102    /// Adds the list of superseded units for the given unit to the graph.
103    fn add_superseded(&mut self, unit_id: Ustr, superseded: &[Ustr]);
104
105    /// Returns the type of the given unit.
106    fn get_unit_type(&self, unit_id: Ustr) -> Option<UnitType>;
107
108    /// Returns the lessons belonging to the given course.
109    fn get_course_lessons(&self, course_id: Ustr) -> Option<Arc<UstrSet>>;
110
111    /// Updates the starting lessons for all courses. The starting lessons of the course are those
112    /// of its lessons that should be practiced first when the course is introduced to the student.
113    /// The scheduler uses them to traverse through the other lessons in the course in the correct
114    /// order. This function should be called once after all the courses and lessons have been added
115    /// to the graph.
116    fn update_starting_lessons(&mut self);
117
118    /// Returns the starting lessons for the given course.
119    fn get_starting_lessons(&self, course_id: Ustr) -> Option<Arc<UstrSet>>;
120
121    /// Returns the course to which the given lesson belongs.
122    fn get_lesson_course(&self, lesson_id: Ustr) -> Option<Ustr>;
123
124    /// Returns the exercises belonging to the given lesson.
125    fn get_lesson_exercises(&self, lesson_id: Ustr) -> Option<Arc<UstrSet>>;
126
127    /// Returns the lesson to which the given exercise belongs.
128    fn get_exercise_lesson(&self, exercise_id: Ustr) -> Option<Ustr>;
129
130    /// Returns the weights of the dependencies of the given unit.
131    fn get_dependencies(&self, unit_id: Ustr) -> Option<Arc<UstrSet>>;
132
133    /// Returns all the units which depend on the given unit.
134    fn get_dependents(&self, unit_id: Ustr) -> Option<Arc<UstrSet>>;
135
136    /// Returns the dependency sinks of the graph. A dependency sink is a unit with no dependencies
137    /// from which a walk of the entire unit graph needs to start. Because the lessons in a course
138    /// implicitly depend on their course, properly initialized lessons do not belong to this set.
139    ///
140    /// This set also includes the units that are mentioned as dependencies of other units but are
141    /// never added to the graph because they are missing from the course library. Those units are
142    /// added as dependency sinks so that the scheduler can reach their dependents, which are part
143    /// of the library.
144    fn get_dependency_sinks(&self) -> Arc<UstrSet>;
145
146    /// Returns the units that this unit encompasses.
147    fn get_encompasses(&self, unit_id: Ustr) -> Option<Vec<(Ustr, f32)>>;
148
149    /// Returns the units that the given unit is encompassed by.
150    fn get_encompassed_by(&self, unit_id: Ustr) -> Option<Vec<(Ustr, f32)>>;
151
152    /// Returns the units that this unit supersedes.
153    fn get_supersedes(&self, unit_id: Ustr) -> Option<Arc<UstrSet>>;
154
155    /// Returns the units that the given unit is superseded by.
156    fn get_superseded_by(&self, unit_id: Ustr) -> Option<Arc<UstrSet>>;
157
158    /// Performs a cycle check on the graph, done currently when opening the Trane library to
159    /// prevent any infinite traversal of the graph and immediately inform the user of the issue.
160    fn check_cycles(&self) -> Result<(), UnitGraphError>;
161
162    /// Generates a DOT graph of the dependent graph. DOT files are used by Graphviz to visualize a
163    /// graph, in this case the dependent graph. This operation was suggested in issue
164    /// [#13](https://github.com/trane-project/trane-cli/issues/13) in the
165    /// [trane-cli](https://github.com/trane-project/trane-cli) repo.
166    ///
167    /// This allows users to have some way to visualize the graph without having to implement such a
168    /// feature and depend on Graphviz instead.
169    ///
170    /// The dependent graph is outputted instead of the dependency graph so that the output is
171    /// easier to read. If you follow the arrows, then you are traversing the path that students
172    /// must take to master a skill.
173    ///
174    /// If courses_only is true, only courses will be included in the graph.
175    fn generate_dot_graph(&self, courses_only: bool) -> String;
176}
177
178/// An implementation of [`UnitGraph`] describing the units and relationships as an adjacency list
179/// stored in hash maps. All of it is stored in memory, as the memory benchmarks show that less than
180/// 20 MB of memory are used even when opening a large Trane library.
181#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
182pub struct InMemoryUnitGraph {
183    /// The mapping of a unit to its type.
184    type_map: UstrMap<UnitType>,
185
186    /// The mapping of a course to its lessons.
187    course_lesson_map: UstrMap<Arc<UstrSet>>,
188
189    /// The mapping of a course to its starting lessons.
190    starting_lessons_map: UstrMap<Arc<UstrSet>>,
191
192    /// The mapping of a lesson to its course.
193    lesson_course_map: UstrMap<Ustr>,
194
195    /// The mapping of a lesson to its exercises.
196    lesson_exercise_map: UstrMap<Arc<UstrSet>>,
197
198    /// The mapping of an exercise to its lesson.
199    exercise_lesson_map: UstrMap<Ustr>,
200
201    /// The mapping of a unit to its dependencies.
202    dependency_graph: UstrMap<Arc<UstrSet>>,
203
204    /// The mapping of a unit to all its dependents.
205    dependent_graph: UstrMap<Arc<UstrSet>>,
206
207    /// The set of all dependency sinks in the graph.
208    dependency_sinks: Arc<UstrSet>,
209
210    /// The mapping of a unit to the units it encompasses as a list of tuples (ID, weight).
211    encompasses_graph: UstrMap<Vec<(Ustr, f32)>>,
212
213    /// The mapping of a unit to the units that encompass it as a list of tuples (ID, weight).
214    encompassed_by: UstrMap<Vec<(Ustr, f32)>>,
215
216    /// The mapping of a unit to the units it supersedes.
217    supersedes_graph: UstrMap<Arc<UstrSet>>,
218
219    /// The mapping of a unit to the units that supersede it.
220    superseded_by: UstrMap<Arc<UstrSet>>,
221}
222
223impl InMemoryUnitGraph {
224    /// Updates the dependency sinks of the given unit when the given unit and dependencies are
225    /// added to the graph.
226    fn update_dependency_sinks(&mut self, unit_id: Ustr, dependencies: &[Ustr]) {
227        // If the current dependencies and the new dependencies are both empty, keep the unit in the
228        // set of dependency sinks. Otherwise, remove it.
229        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        // Remove the unit from the dependency sinks if it's a lesson and its course exists. If the
238        // course is a dependency sink, the lesson is redundant. If the course is not a dependency
239        // sink, the lesson is not a dependency sink either.
240        if self.get_lesson_course(unit_id).is_some() {
241            Arc::make_mut(&mut self.dependency_sinks).remove(&unit_id);
242        }
243
244        // If a course is mentioned as a dependency, but it's missing, it should be a dependency
245        // sink. To ensure this requirement, the function is called recursively on all the
246        // dependents with an empty dependency set. It's safe to do this because a call to this
247        // function for a course with an empty dependency list followed by another with a non-empty
248        // list has the same result as only executing the second call, but makes sure that any
249        // missing courses are added to the dependency sinks.
250        for dependency_id in dependencies {
251            self.update_dependency_sinks(*dependency_id, &[]);
252        }
253    }
254
255    /// Updates the type of the given unit. Returns an error if the unit already had a type, and
256    /// it's different from the type provided in the function call.
257    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    /// Helper function to add a course to the graph.
277    fn add_course_helper(&mut self, course_id: Ustr) -> Result<()> {
278        // Verify the course doesn't already exist.
279        ensure!(
280            !self.type_map.contains_key(&course_id),
281            "course with ID {course_id} already exists",
282        );
283
284        // Add the course to the type map to mark it as existing.
285        self.update_unit_type(course_id, UnitType::Course)?;
286        Ok(())
287    }
288
289    /// Helper function to add a lesson to the graph.
290    fn add_lesson_helper(&mut self, lesson_id: Ustr, course_id: Ustr) -> Result<()> {
291        // Verify the lesson doesn't already exist.
292        ensure!(
293            !self.type_map.contains_key(&lesson_id),
294            "lesson with ID {lesson_id} already exists",
295        );
296
297        // Add the course and lessons to the type map.
298        self.update_unit_type(lesson_id, UnitType::Lesson)?;
299        self.update_unit_type(course_id, UnitType::Course)?;
300
301        // Update the map of lesson to course and course to lessons.
302        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    /// Helper function to add an exercise to the graph.
308    fn add_exercise_helper(&mut self, exercise_id: Ustr, lesson_id: Ustr) -> Result<()> {
309        // Verify the exercise doesn't already exist.
310        ensure!(
311            !self.type_map.contains_key(&exercise_id),
312            "exercise with ID {exercise_id} already exists",
313        );
314
315        // Add the exercise and lesson to the type map.
316        self.update_unit_type(exercise_id, UnitType::Exercise)?;
317        self.update_unit_type(lesson_id, UnitType::Lesson)?;
318
319        // Update the map of exercise to lesson and lesson to exercises.
320        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    // Performs some sanity checks before adding a dependency.
326    #[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    /// Helper function to add dependencies to a unit.
350    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        // Update the dependency sinks and dependency map.
359        self.update_dependency_sinks(unit_id, dependencies);
360        Arc::make_mut(self.dependency_graph.entry(unit_id).or_default()).extend(dependencies);
361
362        // For each dependency, insert the equivalent dependent relationship.
363        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    /// Helper function to add encompassed units to a unit.
370    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        // Compute the full list of encompassed units with their weights. Dependencies not in the
384        // encmpassed list are added with a default weight of 1.0.
385        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        // Update the encompassed and encompassed_by maps.
396        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    /// Checks for cycles a pair of graphs, taking a function to get the neighbors of a node and
410    /// another to check the consistency of the relationships in the two graphs.
411    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        // Perform DFS on the graph to find cycles from each node.
418        let mut visited = UstrSet::default();
419        for unit_id in graph_keys {
420            if visited.contains(unit_id) {
421                continue;
422            }
423
424            // Initialize a path of stacks and pop from it until it's empty.
425            let mut stack: Vec<Vec<Ustr>> = vec![vec![*unit_id]];
426            while let Some(path) = stack.pop() {
427                // Update the visited nodes and skip visited nodes.
428                let current_id = *path.last().unwrap_or(&Ustr::default());
429                if visited.contains(&current_id) {
430                    continue;
431                }
432                visited.insert(current_id);
433
434                // Push new paths to the stack after checking the consistency of the graph and its
435                // reverse.
436                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    // Helper function to check for cycles and consistency in all the graphs.
462    fn check_cycles_helper(&self) -> Result<()> {
463        // Check the dependency and dependent graphs for cycles and consistency.
464        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(&current),
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        // Check the supersedes and superseded_by graphs for cycles and consistency.
484        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(&current),
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        // Check the encompasses and encompassed_by graphs for cycles and consistency.
501        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        // The two graphs are virtually identical, so save space by clearing this graph.
561        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        // Update the superseded map.
571        if superseded.is_empty() {
572            return;
573        }
574        Arc::make_mut(self.supersedes_graph.entry(unit_id).or_default()).extend(superseded);
575
576        // For each superseded ID, insert the equivalent superseding relationship.
577        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        // Find the starting lessons for each course.
596        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                    // The lesson is a starting lesson if the set of lessons in the course and the
604                    // dependencies of this lesson are disjoint.
605                    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            // Before updating the map, the dependency sinks need to be updated as well. The course
614            // is no longer a dependency sink if any of its starting lessons have dependencies to
615            // other valid units in the graph.
616            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        // Use the dependency graph if the graph is empty.
661        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        // Use the dependent graph if the graph is empty.
671        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        // Initialize the output with the first line of the file.
694        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        // Add each course to the DOT graph.
699        for course_id in courses {
700            // Add an entry for the course node and set the color to red.
701            let _ = writeln!(output, "    \"{course_id}\" [color=red, style=filled]");
702
703            // Write the entry in the graph for all the of the dependents of this course. Filter out
704            // lessons if only courses should be added.
705            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            // Add the initial lessons in the course as dependents.
720            //
721            // A course's lessons are not explicitly attached to the graph. This is not exactly
722            // accurate, but properly connecting them in the graph would require each course to have
723            // two nodes, one inbound which is connected to the starting lessons and the course's
724            // dependencies, and one outbound which is connected to the last lessons in the course
725            // (by the order in which they must be traversed to master the entire course) and to the
726            // course's dependents. This might be amended, either here in this function or in the
727            // implementation of the graph itself, but it is not a high priority.
728            if !courses_only {
729                dependents.extend(
730                    self.get_starting_lessons(course_id)
731                        .unwrap_or_default()
732                        .iter(),
733                );
734            }
735
736            // Write an entry for each of the course's dependents.
737            dependents.sort();
738            for dependent in dependents {
739                let _ = writeln!(output, "    \"{course_id}\" -> \"{dependent}\"");
740            }
741
742            // Repeat the same process for each lesson in this course, unless only the courses
743            // should be included.
744            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                // Add an entry for the lesson node and set the color to blue.
756                let _ = writeln!(output, "    \"{lesson_id}\" [color=blue, style=filled]");
757
758                // Add an entry in the graph for all of this lesson's dependents.
759                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        // Close the graph.
773        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    /// Verifies retrieving the correct unit type from the graph.
792    #[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    /// Verifies the basic functionality of the graph, adding course, lessons, and exercises.
803    #[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    /// Verifies retrieving the correct dependencies and dependents from the graph.
858    #[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    /// Verifies that courses whose starting lessons have dependencies to other valid units in the
926    /// graph are not included in the dependency sinks.
927    #[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    /// Verifies retrieving the correct encompassed and encompassed_by units from the graph.
947    #[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    /// Verifies that the dependency graph is used when there is no encompassing graph.
998    #[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    /// Verifies retrieving the correct superseded and superseded_by units from the graph.
1059    #[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    /// Verifies generating a DOT graph.
1100    #[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        // Generate the graph with all units.
1126        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        // Generate the graph with only lessons.
1149        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    /// Verifies that a cycle in the dependencies is detected and causes an error.
1192    #[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        // Add a cycle, which should be detected when calling `check_cycles`.
1212        graph.add_dependencies(course1_id, UnitType::Course, &[course5_id])?;
1213        assert!(graph.check_cycles().is_err());
1214
1215        Ok(())
1216    }
1217
1218    /// Verifies that a cycle in the encompassed graph is detected and causes an error.
1219    #[test]
1220    fn encompassed_cycle() -> Result<()> {
1221        // Add a cycle, which should be detected when calling `check_cycles`.
1222        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    /// Verifies that a cycle in the superseded graph is detected and causes an error.
1238    #[test]
1239    fn superseded_cycle() {
1240        // Add a cycle, which should be detected when calling `check_cycles`.
1241        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    /// Verifies that the cycle check fails if a dependent relationship is missing.
1256    #[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        // Manually remove the dependent relationship to trigger the check and make the cycle
1268        // detection fail.
1269        graph
1270            .dependent_graph
1271            .insert(lesson1_id, Arc::new(UstrSet::default()));
1272        assert!(graph.check_cycles().is_err());
1273        // Also check that the check fails if the dependents value is `None`.
1274        graph.dependency_graph.remove(&lesson1_id);
1275        assert!(graph.check_cycles().is_err());
1276        Ok(())
1277    }
1278
1279    /// Verifies that the cycle check fails if an encompasing relationship is missing.
1280    #[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        // Manually remove the encompasing relationship to trigger the check and make the cycle
1288        // detection fail.
1289        graph.encompassed_by.insert(lesson1_id, Vec::default());
1290        assert!(graph.check_cycles().is_err());
1291        // Also check that the check fails if the encompasing value is `None`.
1292        graph.encompassed_by.remove(&lesson1_id);
1293        assert!(graph.check_cycles().is_err());
1294        Ok(())
1295    }
1296
1297    /// Verifies that the cycle check fails if a superseding relationship is missing.
1298    #[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        // Manually remove the superseding relationship to trigger the check and make the cycle
1306        // detection fail.
1307        graph
1308            .superseded_by
1309            .insert(lesson1_id, Arc::new(UstrSet::default()));
1310        assert!(graph.check_cycles().is_err());
1311        // Also check that the check fails if the superseding value is `None`.
1312        graph.dependency_graph.remove(&lesson1_id);
1313        assert!(graph.check_cycles().is_err());
1314    }
1315}