• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

trane-project / trane / 12735888711

12 Jan 2025 06:06PM UTC coverage: 92.973% (-6.9%) from 99.866%
12735888711

Pull #319

github

web-flow
Merge 2bba403ef into f4478d7eb
Pull Request #319: Update to rust 1.84

69 of 77 new or added lines in 18 files covered. (89.61%)

236 existing lines in 13 files now uncovered.

9672 of 10403 relevant lines covered (92.97%)

87046.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.14
/src/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
//! 1. An exercise, which represents a single task testing a skill which the student is required to
15
//!    assess when practiced.
16
//! 2. A lesson, which represents a collection of exercises which test the same skill and can be
17
//!    practiced in any order.
18
//! 3. A course, a collection of lessons which are related. It mostly exists to help organize the
19
//!    material in larger entities which share some context.
20
//!
21
//! The relationships between the units is one of the following:
22
//! 1. A course or lesson A is a dependency of course or lesson B if A needs to be sufficiently
23
//!    mastered before B can be practiced.
24
//! 2. The reverse relationship. Thus, we say that B is a dependent of A.
25
//! 3. A course or lesson A is superseded by another course or lesson B if sufficient mastery of B
26
//!    makes showing exercises from A redundant.
27
//! 4. The reverse relationship. Thus, we say that B supersedes A.
28
//!
29
//! The graph also provides a number of operations to manipulate the graph, which are only used when
30
//! reading the Trane library (see [`course_library`](crate::course_library)), and another few to
31
//! derive information from the graph ("which are the lessons in a course?" for example). The graph
32
//! is not in any way responsible for how the exercises are scheduled (see
33
//! [`scheduler`](crate::scheduler)) nor it stores any information about a student's practice (see
34
//! [`practice_stats`](crate::practice_stats)) or preferences (see [`blacklist`](crate::blacklist),
35
//! [`filter_manager`](crate::filter_manager) and [`review_list`](crate::review_list)).
36

37
use anyhow::{anyhow, bail, ensure, Result};
38
use std::fmt::Write;
39
use ustr::{Ustr, UstrMap, UstrSet};
40

41
use crate::{data::UnitType, error::UnitGraphError};
42

43
/// Stores the units and their dependency relationships (for lessons and courses only, since
44
/// exercises do not define any dependencies). It provides basic functions to update the graph and
45
/// retrieve information about it for use during scheduling and student's requests.
46
///
47
/// The operations that update the graph are only used when reading the Trane library during
48
/// startup. A user that copies new courses to an existing and currently opened library will need to
49
/// restart Trane to see the changes take effect.
50
pub trait UnitGraph {
51
    /// Adds a new course to the unit graph.
52
    fn add_course(&mut self, course_id: Ustr) -> Result<(), UnitGraphError>;
53

54
    /// Adds a new lesson to the unit graph. It also takes the ID of the course to which this lesson
55
    /// belongs.
56
    fn add_lesson(&mut self, lesson_id: Ustr, course_id: Ustr) -> Result<(), UnitGraphError>;
57

58
    /// Adds a new exercise to the unit graph. It also takes the ID of the lesson to which this
59
    /// exercise belongs.
60
    fn add_exercise(&mut self, exercise_id: Ustr, lesson_id: Ustr) -> Result<(), UnitGraphError>;
61

62
    /// Takes a unit and its dependencies and updates the graph accordingly. Returns an error if
63
    /// `unit_type` is `UnitType::Exercise` as only courses and lessons are allowed to have
64
    /// dependencies. An error is also returned if the unit was not previously added by calling one
65
    /// of `add_course` or `add_lesson`.
66
    fn add_dependencies(
67
        &mut self,
68
        unit_id: Ustr,
69
        unit_type: UnitType,
70
        dependencies: &[Ustr],
71
    ) -> Result<(), UnitGraphError>;
72

73
    /// Adds the list of superseded units for the given unit to the graph.
74
    fn add_superseded(&mut self, unit_id: Ustr, superseded: &[Ustr]);
75

76
    /// Returns the type of the given unit.
77
    fn get_unit_type(&self, unit_id: Ustr) -> Option<UnitType>;
78

79
    /// Returns the lessons belonging to the given course.
80
    fn get_course_lessons(&self, course_id: Ustr) -> Option<UstrSet>;
81

82
    /// Updates the starting lessons for all courses. The starting lessons of the course are those
83
    /// of its lessons that should be practiced first when the course is introduced to the student.
84
    /// The scheduler uses them to traverse through the other lessons in the course in the correct
85
    /// order. This function should be called once after all the courses and lessons have been added
86
    /// to the graph.
87
    fn update_starting_lessons(&mut self);
88

89
    /// Returns the starting lessons for the given course.
90
    fn get_starting_lessons(&self, course_id: Ustr) -> Option<UstrSet>;
91

92
    /// Returns the course to which the given lesson belongs.
93
    fn get_lesson_course(&self, lesson_id: Ustr) -> Option<Ustr>;
94

95
    /// Returns the exercises belonging to the given lesson.
96
    fn get_lesson_exercises(&self, lesson_id: Ustr) -> Option<UstrSet>;
97

98
    /// Returns the lesson to which the given exercise belongs.
99
    fn get_exercise_lesson(&self, exercise_id: Ustr) -> Option<Ustr>;
100

101
    /// Returns the dependencies of the given unit.
102
    fn get_dependencies(&self, unit_id: Ustr) -> Option<UstrSet>;
103

104
    /// Returns all the units which depend on the given unit.
105
    fn get_dependents(&self, unit_id: Ustr) -> Option<UstrSet>;
106

107
    /// Returns the dependency sinks of the graph. A dependency sink is a unit with no dependencies
108
    /// from which a walk of the entire unit graph needs to start. Because the lessons in a course
109
    /// implicitly depend on their course, properly initialized lessons do not belong to this set.
110
    ///
111
    /// This set also includes the units that are mentioned as dependencies of other units but are
112
    /// never added to the graph because they are missing from the course library. Those units are
113
    /// added as dependency sinks so that the scheduler can reach their dependents, which are part
114
    /// of the library.
115
    fn get_dependency_sinks(&self) -> UstrSet;
116

117
    /// Returns the lessons and courses that are superseded by the given lesson or course.
118
    fn get_superseded(&self, unit_id: Ustr) -> Option<UstrSet>;
119

120
    /// Returns the lessons and courses that supersede the given lesson or course.
121
    fn get_superseding(&self, unit_id: Ustr) -> Option<UstrSet>;
122

123
    /// Performs a cycle check on the graph, done currently when opening the Trane library to
124
    /// prevent any infinite traversal of the graph and immediately inform the user of the issue.
125
    fn check_cycles(&self) -> Result<(), UnitGraphError>;
126

127
    /// Generates a DOT graph of the dependent graph. DOT files are used by Graphviz to visualize a
128
    /// graph, in this case the dependent graph. This operation was suggested in issue
129
    /// [#13](https://github.com/trane-project/trane-cli/issues/13) in the
130
    /// [trane-cli](https://github.com/trane-project/trane-cli) repo.
131
    ///
132
    /// This allows users to have some way to visualize the graph without having to implement such a
133
    /// feature and depend on Graphviz instead.
134
    ///
135
    /// The dependent graph is outputted instead of the dependency graph so that the output is
136
    /// easier to read. If you follow the arrows, then you are traversing the path that students
137
    /// must take to master a skill.
138
    fn generate_dot_graph(&self) -> String;
139
}
140

141
/// An implementation of [`UnitGraph`] describing the units and relationships as an adjacency list
142
/// stored in hash maps. All of it is stored in memory, as the memory benchmarks show that less than
143
/// 20 MB of memory are used even when opening a large Trane library.
144
#[derive(Default)]
145
pub struct InMemoryUnitGraph {
146
    /// The mapping of a unit to its type.
147
    type_map: UstrMap<UnitType>,
148

149
    /// The mapping of a course to its lessons.
150
    course_lesson_map: UstrMap<UstrSet>,
151

152
    /// The mapping of a course to its starting lessons.
153
    starting_lessons_map: UstrMap<UstrSet>,
154

155
    /// The mapping of a lesson to its course.
156
    lesson_course_map: UstrMap<Ustr>,
157

158
    /// The mapping of a lesson to its exercises.
159
    lesson_exercise_map: UstrMap<UstrSet>,
160

161
    /// The mapping of an exercise to its lesson.
162
    exercise_lesson_map: UstrMap<Ustr>,
163

164
    /// The mapping of a unit to its dependencies.
165
    dependency_graph: UstrMap<UstrSet>,
166

167
    /// The mapping of a unit to all its dependents.
168
    dependent_graph: UstrMap<UstrSet>,
169

170
    /// The set of all dependency sinks in the graph.
171
    dependency_sinks: UstrSet,
172

173
    /// The mapping of a unit to the units it supersedes.
174
    superseded_graph: UstrMap<UstrSet>,
175

176
    /// The mapping of a unit to the units that supersede it.
177
    superseding_graph: UstrMap<UstrSet>,
178
}
179

180
impl InMemoryUnitGraph {
181
    /// Updates the dependency sinks of the given unit when the given unit and dependencies are
182
    /// added to the graph.
183
    fn update_dependency_sinks(&mut self, unit_id: Ustr, dependencies: &[Ustr]) {
4,399✔
184
        // If the current dependencies and the new dependencies are both empty, keep the unit in the
4,399✔
185
        // set of dependency sinks. Otherwise, remove it.
4,399✔
186
        let empty = UstrSet::default();
4,399✔
187
        let current_dependencies = self.dependency_graph.get(&unit_id).unwrap_or(&empty);
4,399✔
188
        if current_dependencies.is_empty() && dependencies.is_empty() {
4,399✔
189
            self.dependency_sinks.insert(unit_id);
2,286✔
190
        } else {
2,286✔
191
            self.dependency_sinks.remove(&unit_id);
2,113✔
192
        }
2,113✔
193

194
        // Remove the unit from the dependency sinks if it's a lesson and its course exists. If the
195
        // course is a dependency sink, the lesson is redundant. If the course is not a dependency
196
        // sink, the lesson is not a dependency sink either.
197
        if self.get_lesson_course(unit_id).is_some() {
4,399✔
198
            self.dependency_sinks.remove(&unit_id);
2,208✔
199
        }
2,208✔
200

201
        // If a course is mentioned as a dependency, but it's missing, it should be a dependency
202
        // sink. To ensure this requirement, the function is called recursively on all the
203
        // dependents with an empty dependency set. It's safe to do this because a call to this
204
        // function for a course with an empty dependency list followed by another with a non-empty
205
        // list has the same result as only executing the second call, but makes sure that any
206
        // missing courses are added to the dependency sinks.
207
        for dependency_id in dependencies {
6,821✔
208
            self.update_dependency_sinks(*dependency_id, &[]);
2,422✔
209
        }
2,422✔
210
    }
4,399✔
211

212
    /// Updates the type of the given unit. Returns an error if the unit already had a type, and
213
    /// it's different from the type provided in the function call.
214
    fn update_unit_type(&mut self, unit_id: Ustr, unit_type: UnitType) -> Result<()> {
30,038✔
215
        match self.type_map.get(&unit_id) {
30,038✔
216
            None => {
217
                self.type_map.insert(unit_id, unit_type);
15,268✔
218
                Ok(())
15,268✔
219
            }
220
            Some(existing_type) => {
14,770✔
221
                if unit_type == *existing_type {
14,770✔
222
                    Ok(())
14,769✔
223
                } else {
224
                    Err(anyhow!(
1✔
225
                        "cannot update unit type of unit {} from type {:#?}) to {:#?}.",
1✔
226
                        unit_id,
1✔
227
                        existing_type,
1✔
228
                        unit_type
1✔
229
                    ))
1✔
230
                }
231
            }
232
        }
233
    }
30,038✔
234

235
    /// Helper function to add a course to the graph.
236
    fn add_course_helper(&mut self, course_id: Ustr) -> Result<()> {
493✔
237
        // Verify the course doesn't already exist.
493✔
238
        ensure!(
493✔
239
            !self.type_map.contains_key(&course_id),
493✔
240
            "course with ID {} already exists",
1✔
241
            course_id
242
        );
243

244
        // Add the course to the type map to mark it as existing.
245
        self.update_unit_type(course_id, UnitType::Course)?;
492✔
246
        Ok(())
492✔
247
    }
493✔
248

249
    /// Helper function to add a lesson to the graph.
250
    fn add_lesson_helper(&mut self, lesson_id: Ustr, course_id: Ustr) -> Result<()> {
1,491✔
251
        // Verify the lesson doesn't already exist.
1,491✔
252
        ensure!(
1,491✔
253
            !self.type_map.contains_key(&lesson_id),
1,491✔
254
            "lesson with ID {} already exists",
1✔
255
            lesson_id
256
        );
257

258
        // Add the course and lessons to the type map.
259
        self.update_unit_type(lesson_id, UnitType::Lesson)?;
1,490✔
260
        self.update_unit_type(course_id, UnitType::Course)?;
1,490✔
261

262
        // Update the map of lesson to course and course to lessons.
263
        self.lesson_course_map.insert(lesson_id, course_id);
1,490✔
264
        self.course_lesson_map
1,490✔
265
            .entry(course_id)
1,490✔
266
            .or_default()
1,490✔
267
            .insert(lesson_id);
1,490✔
268
        Ok(())
1,490✔
269
    }
1,491✔
270

271
    /// Helper function to add an exercise to the graph.
272
    fn add_exercise_helper(&mut self, exercise_id: Ustr, lesson_id: Ustr) -> Result<()> {
13,283✔
273
        // Verify the exercise doesn't already exist.
13,283✔
274
        ensure!(
13,283✔
275
            !self.type_map.contains_key(&exercise_id),
13,283✔
276
            "exercise with ID {} already exists",
1✔
277
            exercise_id
278
        );
279

280
        // Add the exercise and lesson to the type map.
281
        self.update_unit_type(exercise_id, UnitType::Exercise)?;
13,282✔
282
        self.update_unit_type(lesson_id, UnitType::Lesson)?;
13,282✔
283

284
        // Update the map of exercise to lesson and lesson to exercises.
285
        self.lesson_exercise_map
13,282✔
286
            .entry(lesson_id)
13,282✔
287
            .or_default()
13,282✔
288
            .insert(exercise_id);
13,282✔
289
        self.exercise_lesson_map.insert(exercise_id, lesson_id);
13,282✔
290
        Ok(())
13,282✔
291
    }
13,283✔
292

293
    /// Helper function to add dependencies to a unit.
294
    fn add_dependencies_helper(
1,977✔
295
        &mut self,
1,977✔
296
        unit_id: Ustr,
1,977✔
297
        unit_type: &UnitType,
1,977✔
298
        dependencies: &[Ustr],
1,977✔
299
    ) -> Result<()> {
1,977✔
300
        // Perform some sanity checks on the unit type and dependencies.
1,977✔
301
        ensure!(
1,977✔
302
            *unit_type != UnitType::Exercise,
1,977✔
UNCOV
303
            "exercise {} cannot have dependencies",
×
304
            unit_id,
305
        );
306
        ensure!(
1,977✔
307
            dependencies.iter().all(|dep| *dep != unit_id),
2,422✔
UNCOV
308
            "unit {} cannot depend on itself",
×
309
            unit_id,
310
        );
311

312
        // Verify that the unit was added before trying to list its dependencies.
313
        ensure!(
1,977✔
314
            self.type_map.contains_key(&unit_id),
1,977✔
UNCOV
315
            "unit {} of type {:?} must be explicitly added before adding dependencies",
×
316
            unit_id,
317
            unit_type,
318
        );
319

320
        // Update the dependency sinks and dependency map.
321
        self.update_dependency_sinks(unit_id, dependencies);
1,977✔
322
        self.dependency_graph
1,977✔
323
            .entry(unit_id)
1,977✔
324
            .or_default()
1,977✔
325
            .extend(dependencies);
1,977✔
326

327
        // For each dependency, insert the equivalent dependent relationship.
328
        for dependency_id in dependencies {
4,399✔
329
            self.dependent_graph
2,422✔
330
                .entry(*dependency_id)
2,422✔
331
                .or_default()
2,422✔
332
                .insert(unit_id);
2,422✔
333
        }
2,422✔
334
        Ok(())
1,977✔
335
    }
1,977✔
336

337
    /// Helper function to check for cycles in the dependency graph.
338
    fn check_cycles_helper(&self) -> Result<()> {
78✔
339
        // Perform a depth-first search of the dependency graph from each unit. Return an error if
78✔
340
        // the same unit is encountered twice during the search.
78✔
341
        let mut visited = UstrSet::default();
78✔
342
        for unit_id in self.dependency_graph.keys() {
1,966✔
343
            // The node has been visited, so it can be skipped.
344
            if visited.contains(unit_id) {
1,966✔
345
                continue;
718✔
346
            }
1,248✔
347

1,248✔
348
            // The stacks store a path of traversed units and is initialized with the current unit.
1,248✔
349
            let mut stack: Vec<Vec<Ustr>> = Vec::new();
1,248✔
350
            stack.push(vec![*unit_id]);
1,248✔
351

352
            // Run a depth-first search and stop if a cycle is found or the graph is exhausted.
353
            while let Some(path) = stack.pop() {
4,908✔
354
                // Update the set of visited nodes.
355
                let current_id = *path.last().unwrap_or(&Ustr::default());
3,663✔
356
                if visited.contains(&current_id) {
3,663✔
357
                    continue;
1,607✔
358
                }
2,056✔
359
                visited.insert(current_id);
2,056✔
360

361
                // Get the dependencies of the current node, check that the dependency and dependent
362
                // graph agree with each other, and generate new paths to add to the stack.
363
                if let Some(dependencies) = self.get_dependencies(current_id) {
2,056✔
364
                    for dependency_id in dependencies {
4,384✔
365
                        // Verify that the dependency and dependent graphs agree with each other by
366
                        // checking that this dependency lists the current unit as a dependent.
367
                        let dependents = self.get_dependents(dependency_id);
2,418✔
368
                        let mut missing_dependent = dependents.is_none();
2,418✔
369
                        if let Some(dependents) = dependents {
2,418✔
370
                            if !dependents.contains(&current_id) {
2,418✔
371
                                missing_dependent = true;
2✔
372
                            }
2,416✔
UNCOV
373
                        }
×
374
                        if missing_dependent {
2,418✔
375
                            bail!(
2✔
376
                                "unit {} lists unit {} as a dependency but the dependent \
2✔
377
                                relationship does not exist",
2✔
378
                                current_id,
2✔
379
                                dependency_id
2✔
380
                            );
2✔
381
                        }
2,416✔
382

2,416✔
383
                        // Check for repeated nodes in the path.
2,416✔
384
                        if path.contains(&dependency_id) {
2,416✔
385
                            bail!("cycle in dependency graph detected");
1✔
386
                        }
2,415✔
387

2,415✔
388
                        // Add a new path to the stack.
2,415✔
389
                        let mut new_path = path.clone();
2,415✔
390
                        new_path.push(dependency_id);
2,415✔
391
                        stack.push(new_path);
2,415✔
392
                    }
393
                }
87✔
394
            }
395
        }
396

397
        // Do the same with the graph of superseded units.
398
        let mut visited = UstrSet::default();
75✔
399
        for unit_id in self.superseded_graph.keys() {
75✔
400
            // The node has been visited, so it can be skipped.
401
            if visited.contains(unit_id) {
73✔
402
                continue;
5✔
403
            }
68✔
404

68✔
405
            // The stacks store a path of traversed units and is initialized with the current unit.
68✔
406
            let mut stack: Vec<Vec<Ustr>> = Vec::new();
68✔
407
            stack.push(vec![*unit_id]);
68✔
408

409
            // Run a depth-first search and stop if a cycle is found or the graph is exhausted.
410
            while let Some(path) = stack.pop() {
206✔
411
                // Update the set of visited nodes.
412
                let current_id = *path.last().unwrap_or(&Ustr::default());
141✔
413
                if visited.contains(&current_id) {
141✔
414
                    continue;
5✔
415
                }
136✔
416
                visited.insert(current_id);
136✔
417

418
                // Get the  of the current node, check that the superseded and superseding graphs
419
                // agree with each other, and generate new paths to add to the stack.
420
                if let Some(superseded) = self.get_superseded(current_id) {
136✔
421
                    for superseded_id in superseded {
149✔
422
                        let superseding = self.get_superseding(superseded_id);
76✔
423
                        let mut missing_superseding = superseding.is_none();
76✔
424
                        if let Some(superseding) = superseding {
76✔
425
                            if !superseding.contains(&current_id) {
76✔
426
                                missing_superseding = true;
2✔
427
                            }
74✔
UNCOV
428
                        }
×
429
                        if missing_superseding {
76✔
430
                            bail!(
2✔
431
                                "unit {} lists unit {} as a superseded unit but the superseding \
2✔
432
                                relationship does not exist",
2✔
433
                                current_id,
2✔
434
                                superseded_id
2✔
435
                            );
2✔
436
                        }
74✔
437

74✔
438
                        // Check for repeated nodes in the path.
74✔
439
                        if path.contains(&superseded_id) {
74✔
440
                            bail!("cycle in superseded graph detected");
1✔
441
                        }
73✔
442

73✔
443
                        // Add a new path to the stack.
73✔
444
                        let mut new_path = path.clone();
73✔
445
                        new_path.push(superseded_id);
73✔
446
                        stack.push(new_path);
73✔
447
                    }
448
                }
60✔
449
            }
450
        }
451
        Ok(())
72✔
452
    }
78✔
453
}
454

455
impl UnitGraph for InMemoryUnitGraph {
456
    fn add_course(&mut self, course_id: Ustr) -> Result<(), UnitGraphError> {
493✔
457
        self.add_course_helper(course_id)
493✔
458
            .map_err(|e| UnitGraphError::AddUnit(course_id, UnitType::Course, e))
493✔
459
    }
493✔
460

461
    fn add_lesson(&mut self, lesson_id: Ustr, course_id: Ustr) -> Result<(), UnitGraphError> {
1,491✔
462
        self.add_lesson_helper(lesson_id, course_id)
1,491✔
463
            .map_err(|e| UnitGraphError::AddUnit(lesson_id, UnitType::Lesson, e))
1,491✔
464
    }
1,491✔
465

466
    fn add_exercise(&mut self, exercise_id: Ustr, lesson_id: Ustr) -> Result<(), UnitGraphError> {
13,283✔
467
        self.add_exercise_helper(exercise_id, lesson_id)
13,283✔
468
            .map_err(|e| UnitGraphError::AddUnit(exercise_id, UnitType::Exercise, e))
13,283✔
469
    }
13,283✔
470

471
    fn add_dependencies(
1,977✔
472
        &mut self,
1,977✔
473
        unit_id: Ustr,
1,977✔
474
        unit_type: UnitType,
1,977✔
475
        dependencies: &[Ustr],
1,977✔
476
    ) -> Result<(), UnitGraphError> {
1,977✔
477
        self.add_dependencies_helper(unit_id, &unit_type, dependencies)
1,977✔
478
            .map_err(|e| UnitGraphError::AddDependencies(unit_id, unit_type, e))
1,977✔
479
    }
1,977✔
480

481
    fn add_superseded(&mut self, unit_id: Ustr, superseded: &[Ustr]) {
1,964✔
482
        // Update the superseded map.
1,964✔
483
        if superseded.is_empty() {
1,964✔
484
            return;
1,888✔
485
        }
76✔
486
        self.superseded_graph
76✔
487
            .entry(unit_id)
76✔
488
            .or_default()
76✔
489
            .extend(superseded);
76✔
490

491
        // For each superseded ID, insert the equivalent superseding relationship.
492
        for superseded_id in superseded {
152✔
493
            self.superseding_graph
76✔
494
                .entry(*superseded_id)
76✔
495
                .or_default()
76✔
496
                .insert(unit_id);
76✔
497
        }
76✔
498
    }
1,964✔
499

500
    fn get_unit_type(&self, unit_id: Ustr) -> Option<UnitType> {
4,971,155✔
501
        self.type_map.get(&unit_id).cloned()
4,971,155✔
502
    }
4,971,155✔
503

504
    fn get_course_lessons(&self, course_id: Ustr) -> Option<UstrSet> {
291,663✔
505
        self.course_lesson_map.get(&course_id).cloned()
291,663✔
506
    }
291,663✔
507

508
    fn get_starting_lessons(&self, course_id: Ustr) -> Option<UstrSet> {
320,639✔
509
        self.starting_lessons_map.get(&course_id).cloned()
320,639✔
510
    }
320,639✔
511

512
    fn update_starting_lessons(&mut self) {
72✔
513
        // Find the starting lessons for each course.
72✔
514
        let empty = UstrSet::default();
72✔
515
        for course_id in self.course_lesson_map.keys() {
445✔
516
            let lessons = self.course_lesson_map.get(course_id).unwrap_or(&empty);
445✔
517
            let starting_lessons = lessons
445✔
518
                .iter()
445✔
519
                .copied()
445✔
520
                .filter(|lesson_id| {
1,485✔
521
                    // The lesson is a starting lesson if the set of lessons in the course and the
1,485✔
522
                    // dependencies of this lesson are disjoint.
1,485✔
523
                    let dependencies = self.get_dependencies(*lesson_id);
1,485✔
524
                    match dependencies {
1,485✔
525
                        None => true,
3✔
526
                        Some(dependencies) => lessons.is_disjoint(&dependencies),
1,482✔
527
                    }
528
                })
1,485✔
529
                .collect();
445✔
530

445✔
531
            self.starting_lessons_map
445✔
532
                .insert(*course_id, starting_lessons);
445✔
533
        }
445✔
534
    }
72✔
535

536
    fn get_lesson_course(&self, lesson_id: Ustr) -> Option<Ustr> {
3,404,180✔
537
        self.lesson_course_map.get(&lesson_id).copied()
3,404,180✔
538
    }
3,404,180✔
539

540
    fn get_lesson_exercises(&self, lesson_id: Ustr) -> Option<UstrSet> {
602,438✔
541
        self.lesson_exercise_map.get(&lesson_id).cloned()
602,438✔
542
    }
602,438✔
543

544
    fn get_exercise_lesson(&self, exercise_id: Ustr) -> Option<Ustr> {
79,659✔
545
        self.exercise_lesson_map.get(&exercise_id).copied()
79,659✔
546
    }
79,659✔
547

548
    fn get_dependencies(&self, unit_id: Ustr) -> Option<UstrSet> {
1,161,844✔
549
        self.dependency_graph.get(&unit_id).cloned()
1,161,844✔
550
    }
1,161,844✔
551

552
    fn get_dependents(&self, unit_id: Ustr) -> Option<UstrSet> {
642,489✔
553
        self.dependent_graph.get(&unit_id).cloned()
642,489✔
554
    }
642,489✔
555

556
    fn get_dependency_sinks(&self) -> UstrSet {
7,471✔
557
        self.dependency_sinks.clone()
7,471✔
558
    }
7,471✔
559

560
    fn get_superseded(&self, unit_id: Ustr) -> Option<UstrSet> {
136✔
561
        self.superseded_graph.get(&unit_id).cloned()
136✔
562
    }
136✔
563

564
    fn get_superseding(&self, unit_id: Ustr) -> Option<UstrSet> {
3,223,081✔
565
        self.superseding_graph.get(&unit_id).cloned()
3,223,081✔
566
    }
3,223,081✔
567

568
    fn check_cycles(&self) -> Result<(), UnitGraphError> {
78✔
569
        self.check_cycles_helper()
78✔
570
            .map_err(UnitGraphError::CheckCycles)
78✔
571
    }
78✔
572

573
    fn generate_dot_graph(&self) -> String {
1✔
574
        // Initialize the output with the first line of the file.
1✔
575
        let mut output = String::from("digraph dependent_graph {\n");
1✔
576
        let mut courses = self.course_lesson_map.keys().copied().collect::<Vec<_>>();
1✔
577
        courses.sort();
1✔
578

579
        // Add each course to the DOT graph.
580
        for course_id in courses {
4✔
581
            // Add an entry for the course node and set the color to red.
582
            let _ = writeln!(output, "    \"{course_id}\" [color=red, style=filled]");
3✔
583

3✔
584
            // Write the entry in the graph for all the of the dependents of this course.
3✔
585
            let mut dependents = self
3✔
586
                .get_dependents(course_id)
3✔
587
                .unwrap_or_default()
3✔
588
                .into_iter()
3✔
589
                .collect::<Vec<_>>();
3✔
590

3✔
591
            // Write the entry in the graph for all the lessons in the course.
3✔
592
            //
3✔
593
            // A course's lessons are not explicitly attached to the graph. This is not exactly
3✔
594
            // accurate, but properly connecting them in the graph would require each course to have
3✔
595
            // two nodes, one inbound which is connected to the starting lessons and the course's
3✔
596
            // dependencies, and one outbound which is connected to the last lessons in the course
3✔
597
            // (by the order in which they must be traversed to master the entire course) and to the
3✔
598
            // course's dependents. This might be amended, either here in this function or in the
3✔
599
            // implementation of the graph itself, but it is not a high priority.
3✔
600
            dependents.extend(
3✔
601
                self.get_starting_lessons(course_id)
3✔
602
                    .unwrap_or_default()
3✔
603
                    .iter(),
3✔
604
            );
3✔
605
            dependents.sort();
3✔
606
            for dependent in dependents {
8✔
607
                let _ = writeln!(output, "    \"{course_id}\" -> \"{dependent}\"");
5✔
608
            }
5✔
609

610
            // Repeat the same process for each lesson in this course.
611
            let mut lessons = self
3✔
612
                .get_course_lessons(course_id)
3✔
613
                .unwrap_or_default()
3✔
614
                .into_iter()
3✔
615
                .collect::<Vec<_>>();
3✔
616
            lessons.sort();
3✔
617
            for lesson_id in lessons {
8✔
618
                // Add an entry for the lesson node and set the color to blue.
619
                let _ = writeln!(output, "    \"{lesson_id}\" [color=blue, style=filled]");
5✔
620

5✔
621
                // Add an entry in the graph for all of this lesson's dependents.
5✔
622
                let mut dependents = self
5✔
623
                    .get_dependents(lesson_id)
5✔
624
                    .unwrap_or_default()
5✔
625
                    .into_iter()
5✔
626
                    .collect::<Vec<_>>();
5✔
627
                dependents.sort();
5✔
628
                for dependent in dependents {
7✔
629
                    let _ = writeln!(output, "    \"{lesson_id}\" -> \"{dependent}\"");
2✔
630
                }
2✔
631
            }
632
        }
633

634
        // Close the graph.
635
        output.push_str("}\n");
1✔
636
        output
1✔
637
    }
1✔
638
}
639

640
#[cfg(test)]
641
mod test {
642
    use anyhow::Result;
643
    use indoc::indoc;
644
    use ustr::{Ustr, UstrSet};
645

646
    use crate::{
647
        data::UnitType,
648
        graph::{InMemoryUnitGraph, UnitGraph},
649
    };
650

651
    /// Verifies retrieving the correct unit type from the graph.
652
    #[test]
653
    fn get_unit_type() -> Result<()> {
1✔
654
        let mut graph = InMemoryUnitGraph::default();
1✔
655
        let id = Ustr::from("id1");
1✔
656
        graph.add_course(id)?;
1✔
657
        graph.add_dependencies(id, UnitType::Course, &[])?;
1✔
658
        assert_eq!(graph.get_unit_type(id), Some(UnitType::Course));
1✔
659
        Ok(())
1✔
660
    }
1✔
661

662
    /// Verifies the basic functionality of the graph, adding course, lessons, and exercises.
663
    #[test]
664
    fn get_course_lessons_and_exercises() -> Result<()> {
1✔
665
        let mut graph = InMemoryUnitGraph::default();
1✔
666
        let course_id = Ustr::from("course1");
1✔
667
        let lesson1_id = Ustr::from("course1::lesson1");
1✔
668
        let lesson2_id = Ustr::from("course1::lesson2");
1✔
669
        let lesson1_exercise1_id = Ustr::from("course1::lesson1::exercise1");
1✔
670
        let lesson1_exercise2_id = Ustr::from("course1::lesson1::exercise2");
1✔
671
        let lesson2_exercise1_id = Ustr::from("course1::lesson2::exercise1");
1✔
672
        let lesson2_exercise2_id = Ustr::from("course1::lesson2::exercise2");
1✔
673

1✔
674
        graph.add_course(course_id)?;
1✔
675
        graph.add_dependencies(course_id, UnitType::Course, &[])?;
1✔
676
        graph.add_lesson(lesson1_id, course_id)?;
1✔
677
        graph.add_exercise(lesson1_exercise1_id, lesson1_id)?;
1✔
678
        graph.add_exercise(lesson1_exercise2_id, lesson1_id)?;
1✔
679
        graph.add_lesson(lesson2_id, course_id)?;
1✔
680
        graph.add_exercise(lesson2_exercise1_id, lesson2_id)?;
1✔
681
        graph.add_exercise(lesson2_exercise2_id, lesson2_id)?;
1✔
682

683
        let course_lessons = graph.get_course_lessons(course_id).unwrap();
1✔
684
        assert_eq!(course_lessons.len(), 2);
1✔
685
        assert!(course_lessons.contains(&lesson1_id));
1✔
686
        assert!(course_lessons.contains(&lesson2_id));
1✔
687

688
        let lesson1_exercises = graph.get_lesson_exercises(lesson1_id).unwrap();
1✔
689
        assert_eq!(lesson1_exercises.len(), 2);
1✔
690
        assert!(lesson1_exercises.contains(&lesson1_exercise1_id));
1✔
691
        assert!(lesson1_exercises.contains(&lesson1_exercise2_id));
1✔
692
        assert_eq!(
1✔
693
            graph.get_exercise_lesson(lesson1_exercise1_id).unwrap(),
1✔
694
            lesson1_id
1✔
695
        );
1✔
696
        assert_eq!(
1✔
697
            graph.get_exercise_lesson(lesson1_exercise2_id).unwrap(),
1✔
698
            lesson1_id
1✔
699
        );
1✔
700

701
        let lesson2_exercises = graph.get_lesson_exercises(lesson2_id).unwrap();
1✔
702
        assert_eq!(lesson2_exercises.len(), 2);
1✔
703
        assert!(lesson2_exercises.contains(&lesson2_exercise1_id));
1✔
704
        assert!(lesson2_exercises.contains(&lesson2_exercise2_id));
1✔
705
        assert_eq!(
1✔
706
            graph.get_exercise_lesson(lesson2_exercise1_id).unwrap(),
1✔
707
            lesson2_id
1✔
708
        );
1✔
709
        assert_eq!(
1✔
710
            graph.get_exercise_lesson(lesson2_exercise2_id).unwrap(),
1✔
711
            lesson2_id
1✔
712
        );
1✔
713

714
        Ok(())
1✔
715
    }
1✔
716

717
    /// Verifies retrieving the correct dependencies and dependents from the graph.
718
    #[test]
719
    fn dependencies() -> Result<()> {
1✔
720
        let mut graph = InMemoryUnitGraph::default();
1✔
721
        let course1_id = Ustr::from("course1");
1✔
722
        let course2_id = Ustr::from("course2");
1✔
723
        let course3_id = Ustr::from("course3");
1✔
724
        let course4_id = Ustr::from("course4");
1✔
725
        let course5_id = Ustr::from("course5");
1✔
726
        graph.add_course(course1_id)?;
1✔
727
        graph.add_course(course2_id)?;
1✔
728
        graph.add_course(course3_id)?;
1✔
729
        graph.add_course(course4_id)?;
1✔
730
        graph.add_course(course5_id)?;
1✔
731
        graph.add_dependencies(course1_id, UnitType::Course, &[])?;
1✔
732
        graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
1✔
733
        graph.add_dependencies(course3_id, UnitType::Course, &[course1_id])?;
1✔
734
        graph.add_dependencies(course4_id, UnitType::Course, &[course2_id])?;
1✔
735
        graph.add_dependencies(course5_id, UnitType::Course, &[course3_id])?;
1✔
736

737
        {
738
            let dependents = graph.get_dependents(course1_id).unwrap();
1✔
739
            assert_eq!(dependents.len(), 2);
1✔
740
            assert!(dependents.contains(&course2_id));
1✔
741
            assert!(dependents.contains(&course3_id));
1✔
742

743
            assert!(graph.get_dependencies(course1_id).unwrap().is_empty());
1✔
744
        }
745

746
        {
747
            let dependents = graph.get_dependents(course2_id).unwrap();
1✔
748
            assert_eq!(dependents.len(), 1);
1✔
749
            assert!(dependents.contains(&course4_id));
1✔
750

751
            let dependencies = graph.get_dependencies(course2_id).unwrap();
1✔
752
            assert_eq!(dependencies.len(), 1);
1✔
753
            assert!(dependencies.contains(&course1_id));
1✔
754
        }
755

756
        {
757
            let dependents = graph.get_dependents(course2_id).unwrap();
1✔
758
            assert_eq!(dependents.len(), 1);
1✔
759
            assert!(dependents.contains(&course4_id));
1✔
760

761
            let dependencies = graph.get_dependencies(course2_id).unwrap();
1✔
762
            assert_eq!(dependencies.len(), 1);
1✔
763
            assert!(dependencies.contains(&course1_id));
1✔
764
        }
765

766
        {
767
            let dependents = graph.get_dependents(course3_id).unwrap();
1✔
768
            assert_eq!(dependents.len(), 1);
1✔
769
            assert!(dependents.contains(&course5_id));
1✔
770

771
            let dependencies = graph.get_dependencies(course3_id).unwrap();
1✔
772
            assert_eq!(dependencies.len(), 1);
1✔
773
            assert!(dependencies.contains(&course1_id));
1✔
774
        }
775

776
        {
777
            assert!(graph.get_dependents(course4_id).is_none());
1✔
778

779
            let dependencies = graph.get_dependencies(course4_id).unwrap();
1✔
780
            assert_eq!(dependencies.len(), 1);
1✔
781
            assert!(dependencies.contains(&course2_id));
1✔
782
        }
783

784
        {
785
            assert!(graph.get_dependents(course5_id).is_none());
1✔
786

787
            let dependencies = graph.get_dependencies(course5_id).unwrap();
1✔
788
            assert_eq!(dependencies.len(), 1);
1✔
789
            assert!(dependencies.contains(&course3_id));
1✔
790
        }
791

792
        let sinks = graph.get_dependency_sinks();
1✔
793
        assert_eq!(sinks.len(), 1);
1✔
794
        assert!(sinks.contains(&course1_id));
1✔
795

796
        graph.check_cycles()?;
1✔
797
        Ok(())
1✔
798
    }
1✔
799

800
    /// Verifies generating a DOT graph.
801
    #[test]
802
    fn generate_dot_graph() -> Result<()> {
1✔
803
        let mut graph = InMemoryUnitGraph::default();
1✔
804
        let course1_id = Ustr::from("1");
1✔
805
        let course1_lesson1_id = Ustr::from("1::1");
1✔
806
        let course1_lesson2_id = Ustr::from("1::2");
1✔
807
        let course2_id = Ustr::from("2");
1✔
808
        let course2_lesson1_id = Ustr::from("2::1");
1✔
809
        let course3_id = Ustr::from("3");
1✔
810
        let course3_lesson1_id = Ustr::from("3::1");
1✔
811
        let course3_lesson2_id = Ustr::from("3::2");
1✔
812

1✔
813
        graph.add_lesson(course1_lesson1_id, course1_id)?;
1✔
814
        graph.add_lesson(course1_lesson2_id, course1_id)?;
1✔
815
        graph.add_lesson(course2_lesson1_id, course2_id)?;
1✔
816
        graph.add_lesson(course3_lesson1_id, course3_id)?;
1✔
817
        graph.add_lesson(course3_lesson2_id, course3_id)?;
1✔
818

819
        graph.add_dependencies(course1_id, UnitType::Course, &[])?;
1✔
820
        graph.add_dependencies(course1_lesson2_id, UnitType::Lesson, &[course1_lesson1_id])?;
1✔
821
        graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
1✔
822
        graph.add_dependencies(course3_id, UnitType::Course, &[course2_id])?;
1✔
823
        graph.add_dependencies(course3_lesson2_id, UnitType::Lesson, &[course3_lesson1_id])?;
1✔
824
        graph.update_starting_lessons();
1✔
825

1✔
826
        let dot = graph.generate_dot_graph();
1✔
827
        let expected = indoc! {r#"
1✔
828
            digraph dependent_graph {
1✔
829
                "1" [color=red, style=filled]
1✔
830
                "1" -> "1::1"
1✔
831
                "1" -> "2"
1✔
832
                "1::1" [color=blue, style=filled]
1✔
833
                "1::1" -> "1::2"
1✔
834
                "1::2" [color=blue, style=filled]
1✔
835
                "2" [color=red, style=filled]
1✔
836
                "2" -> "2::1"
1✔
837
                "2" -> "3"
1✔
838
                "2::1" [color=blue, style=filled]
1✔
839
                "3" [color=red, style=filled]
1✔
840
                "3" -> "3::1"
1✔
841
                "3::1" [color=blue, style=filled]
1✔
842
                "3::1" -> "3::2"
1✔
843
                "3::2" [color=blue, style=filled]
1✔
844
            }
1✔
845
    "#};
1✔
846
        assert_eq!(dot, expected);
1✔
847
        Ok(())
1✔
848
    }
1✔
849

850
    #[test]
851
    fn duplicate_ids() -> Result<()> {
1✔
852
        let mut graph = InMemoryUnitGraph::default();
1✔
853

1✔
854
        let course_id = Ustr::from("course_id");
1✔
855
        graph.add_course(course_id)?;
1✔
856
        let _ = graph.add_course(course_id).is_err();
1✔
857

1✔
858
        let lesson_id = Ustr::from("lesson_id");
1✔
859
        graph.add_lesson(lesson_id, course_id)?;
1✔
860
        let _ = graph.add_lesson(lesson_id, course_id).is_err();
1✔
861

1✔
862
        let exercise_id = Ustr::from("exercise_id");
1✔
863
        graph.add_exercise(exercise_id, lesson_id)?;
1✔
864
        let _ = graph.add_exercise(exercise_id, lesson_id).is_err();
1✔
865

1✔
866
        Ok(())
1✔
867
    }
1✔
868

869
    #[test]
870
    fn update_unit_type_different_types() -> Result<()> {
1✔
871
        let mut graph = InMemoryUnitGraph::default();
1✔
872
        let unit_id = Ustr::from("unit_id");
1✔
873
        graph.update_unit_type(unit_id, UnitType::Course)?;
1✔
874
        assert!(graph.update_unit_type(unit_id, UnitType::Lesson).is_err());
1✔
875
        Ok(())
1✔
876
    }
1✔
877

878
    /// Verifies that a cycle in the dependencies is detected and causes an error.
879
    #[test]
880
    fn dependencies_cycle() -> Result<()> {
1✔
881
        let mut graph = InMemoryUnitGraph::default();
1✔
882
        let course1_id = Ustr::from("course1");
1✔
883
        let course2_id = Ustr::from("course2");
1✔
884
        let course3_id = Ustr::from("course3");
1✔
885
        let course4_id = Ustr::from("course4");
1✔
886
        let course5_id = Ustr::from("course5");
1✔
887
        graph.add_course(course1_id)?;
1✔
888
        graph.add_course(course2_id)?;
1✔
889
        graph.add_course(course3_id)?;
1✔
890
        graph.add_course(course4_id)?;
1✔
891
        graph.add_course(course5_id)?;
1✔
892
        graph.add_dependencies(course1_id, UnitType::Course, &[])?;
1✔
893
        graph.add_dependencies(course2_id, UnitType::Course, &[course1_id])?;
1✔
894
        graph.add_dependencies(course3_id, UnitType::Course, &[course1_id])?;
1✔
895
        graph.add_dependencies(course4_id, UnitType::Course, &[course2_id])?;
1✔
896
        graph.add_dependencies(course5_id, UnitType::Course, &[course3_id])?;
1✔
897

898
        // Add a cycle, which should be detected when calling `check_cycles`.
899
        graph.add_dependencies(course1_id, UnitType::Course, &[course5_id])?;
1✔
900
        assert!(graph.check_cycles().is_err());
1✔
901

902
        Ok(())
1✔
903
    }
1✔
904

905
    /// Verifies that a cycle in the superseded graph is detected and causes an error.
906
    #[test]
907
    fn superseded_cycle() {
1✔
908
        // Add a cycle, which should be detected when calling `check_cycles`.
1✔
909
        let mut graph = InMemoryUnitGraph::default();
1✔
910
        let course1_id = Ustr::from("course1");
1✔
911
        let course2_id = Ustr::from("course2");
1✔
912
        let course3_id = Ustr::from("course3");
1✔
913
        let course4_id = Ustr::from("course4");
1✔
914
        let course5_id = Ustr::from("course5");
1✔
915
        graph.add_superseded(course2_id, &[course1_id]);
1✔
916
        graph.add_superseded(course3_id, &[course1_id]);
1✔
917
        graph.add_superseded(course4_id, &[course2_id]);
1✔
918
        graph.add_superseded(course5_id, &[course3_id]);
1✔
919
        graph.add_superseded(course1_id, &[course5_id]);
1✔
920
        assert!(graph.check_cycles().is_err());
1✔
921
    }
1✔
922

923
    /// Verifies that the cycle check fails if a dependent relationship is missing.
924
    #[test]
925
    fn missing_dependent_relationship() -> Result<()> {
1✔
926
        let mut graph = InMemoryUnitGraph::default();
1✔
927
        let course_id = Ustr::from("course_id");
1✔
928
        let lesson1_id = Ustr::from("lesson1_id");
1✔
929
        let lesson2_id = Ustr::from("lesson2_id");
1✔
930
        graph.add_course(course_id).unwrap();
1✔
931
        graph.add_lesson(lesson1_id, course_id).unwrap();
1✔
932
        graph.add_lesson(lesson2_id, course_id).unwrap();
1✔
933
        graph.add_dependencies(lesson2_id, UnitType::Lesson, &[lesson1_id])?;
1✔
934

935
        // Manually remove the dependent relationship to trigger the check and make the cycle
936
        // detection fail.
937
        graph.dependent_graph.insert(lesson1_id, UstrSet::default());
1✔
938
        assert!(graph.check_cycles().is_err());
1✔
939
        // Also check that the check fails if the dependents value is `None`.
940
        graph.dependency_graph.remove(&lesson1_id);
1✔
941
        assert!(graph.check_cycles().is_err());
1✔
942
        Ok(())
1✔
943
    }
1✔
944

945
    /// Verifies that the cycle check fails if a superseding relationship is missing.
946
    #[test]
947
    fn missing_superseding_relationship() {
1✔
948
        let mut graph = InMemoryUnitGraph::default();
1✔
949
        let lesson1_id = Ustr::from("lesson1_id");
1✔
950
        let lesson2_id = Ustr::from("lesson2_id");
1✔
951
        graph.add_superseded(lesson2_id, &[lesson1_id]);
1✔
952

1✔
953
        // Manually remove the superseding relationship to trigger the check and make the cycle
1✔
954
        // detection fail.
1✔
955
        graph
1✔
956
            .superseding_graph
1✔
957
            .insert(lesson1_id, UstrSet::default());
1✔
958
        assert!(graph.check_cycles().is_err());
1✔
959
        // Also check that the check fails if the superseding value is `None`.
960
        graph.dependency_graph.remove(&lesson1_id);
1✔
961
        assert!(graph.check_cycles().is_err());
1✔
962
    }
1✔
963
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc