Skip to main content

trane/scheduler/
data.rs

1//! Defines the data used by the scheduler and several convenience functions.
2
3use anyhow::{Ok, Result, anyhow};
4use chrono::{DateTime, Utc};
5use parking_lot::RwLock;
6use std::sync::Arc;
7use ustr::{Ustr, UstrMap, UstrSet};
8
9use crate::{
10    blacklist::Blacklist,
11    course_library::CourseLibrary,
12    data::{
13        CourseManifest, ExerciseManifest, LessonManifest, MasteryScore, SchedulerOptions, UnitType,
14        filter::{KeyValueFilter, SavedFilter, SessionPart, StudySessionData, UnitFilter},
15    },
16    filter_manager::FilterManager,
17    graph::UnitGraph,
18    practice_rewards::PracticeRewards,
19    practice_stats::PracticeStats,
20    review_list::ReviewList,
21};
22
23/// A struct encapsulating all the state needed by the scheduler.
24#[derive(Clone)]
25pub struct SchedulerData {
26    /// The options used to run this scheduler.
27    pub options: SchedulerOptions,
28
29    /// The course library storing manifests and info about units.
30    pub course_library: Arc<RwLock<dyn CourseLibrary>>,
31
32    /// The dependency graph of courses and lessons.
33    pub unit_graph: Arc<RwLock<dyn UnitGraph>>,
34
35    /// The list of previous exercise results.
36    pub practice_stats: Arc<RwLock<dyn PracticeStats>>,
37
38    /// The rewards for units based on performance of related units.
39    pub practice_rewards: Arc<RwLock<dyn PracticeRewards>>,
40
41    /// The list of units to skip during scheduling.
42    pub blacklist: Arc<RwLock<dyn Blacklist>>,
43
44    /// The list of units which should be reviewed by the student.
45    pub review_list: Arc<RwLock<dyn ReviewList>>,
46
47    /// The manager used to access unit filters saved by the user.
48    pub filter_manager: Arc<RwLock<dyn FilterManager>>,
49
50    /// A map storing the number of times an exercise has been scheduled during the lifetime of this
51    /// scheduler. The value is used to give more weight during filtering to exercises that have
52    /// been scheduled less often.
53    pub frequency_map: Arc<RwLock<UstrMap<usize>>>,
54
55    /// The number of (successful, failed) exercises during the session.
56    pub trial_counts: Arc<RwLock<(usize, usize)>>,
57}
58
59impl SchedulerData {
60    /// Returns the ID of the lesson to which the exercise with the given ID belongs.
61    #[inline]
62    pub fn get_lesson_id(&self, exercise_id: Ustr) -> Result<Ustr> {
63        self.unit_graph
64            .read()
65            .get_exercise_lesson(exercise_id)
66            .ok_or(anyhow!(
67                "missing lesson ID for exercise with ID {exercise_id}"
68            ))
69    }
70
71    /// Returns the ID of the course to which the lesson with the given ID belongs.
72    #[inline]
73    pub fn get_course_id(&self, lesson_id: Ustr) -> Result<Ustr> {
74        self.unit_graph
75            .read()
76            .get_lesson_course(lesson_id)
77            .ok_or(anyhow!("missing course ID for lesson with ID {lesson_id}"))
78    }
79
80    /// Returns the type of the given unit.
81    #[inline]
82    #[must_use]
83    pub fn get_unit_type(&self, unit_id: Ustr) -> Option<UnitType> {
84        self.unit_graph.read().get_unit_type(unit_id)
85    }
86
87    /// Returns the type of the given unit. Returns an error if the type is unknown.
88    #[inline]
89    pub fn get_unit_type_strict(&self, unit_id: Ustr) -> Result<UnitType> {
90        self.unit_graph
91            .read()
92            .get_unit_type(unit_id)
93            .ok_or(anyhow!("missing unit type for unit with ID {unit_id}"))
94    }
95
96    /// Returns the manifest for the course with the given ID.
97    #[inline]
98    pub fn get_course_manifest(&self, course_id: Ustr) -> Result<Arc<CourseManifest>> {
99        self.course_library
100            .read()
101            .get_course_manifest(course_id)
102            .ok_or(anyhow!("missing manifest for course with ID {course_id}"))
103    }
104
105    /// Returns the manifest for the lesson with the given ID.
106    #[inline]
107    pub fn get_lesson_manifest(&self, lesson_id: Ustr) -> Result<Arc<LessonManifest>> {
108        self.course_library
109            .read()
110            .get_lesson_manifest(lesson_id)
111            .ok_or(anyhow!("missing manifest for lesson with ID {lesson_id}"))
112    }
113
114    /// Returns the manifest for the exercise with the given ID.
115    #[inline]
116    pub fn get_exercise_manifest(&self, exercise_id: Ustr) -> Result<Arc<ExerciseManifest>> {
117        self.course_library
118            .read()
119            .get_exercise_manifest(exercise_id)
120            .ok_or(anyhow!(
121                "missing manifest for exercise with ID {exercise_id}"
122            ))
123    }
124
125    /// Returns whether the unit with the given ID is blacklisted.
126    #[inline]
127    pub fn blacklisted(&self, unit_id: Ustr) -> Result<bool> {
128        let blacklisted = self.blacklist.read().blacklisted(unit_id)?;
129        Ok(blacklisted)
130    }
131
132    /// Returns whether the exercise with the given ID is blacklisted or inside a blacklisted unit.
133    pub fn inside_blacklisted(&self, exercise_id: Ustr) -> Result<bool> {
134        // Check if the exercise itself is blacklisted.
135        let blacklist = self.blacklist.read();
136        let blacklisted = blacklist.blacklisted(exercise_id)?;
137        if blacklisted {
138            return Ok(true);
139        }
140
141        // Check whether the lesson and course to which the exercise belongs are blacklisted.
142        let lesson_id = self.get_lesson_id(exercise_id).unwrap_or_default();
143        let lesson_blacklisted = blacklist.blacklisted(lesson_id)?;
144        let course_id = self.get_course_id(lesson_id).unwrap_or_default();
145        let course_blacklisted = blacklist.blacklisted(course_id)?;
146        Ok(lesson_blacklisted || course_blacklisted)
147    }
148
149    /// Returns all the units that are dependencies of the unit with the given ID.
150    #[inline]
151    #[must_use]
152    pub fn get_all_dependents(&self, unit_id: Ustr) -> Vec<Ustr> {
153        return self
154            .unit_graph
155            .read()
156            .get_dependents(unit_id)
157            .unwrap_or_default()
158            .iter()
159            .copied()
160            .collect();
161    }
162
163    /// Returns all the units that supersede the unit with the given ID.
164    #[inline]
165    #[must_use]
166    pub fn get_superseding(&self, unit_id: Ustr) -> Option<Arc<UstrSet>> {
167        return self.unit_graph.read().get_superseded_by(unit_id);
168    }
169
170    /// Returns all the dependencies of the unit with the given ID at the given depth.
171    #[must_use]
172    pub fn get_dependencies_at_depth(&self, unit_id: Ustr, depth: usize) -> Vec<Ustr> {
173        // Search for the dependencies at the given depth.
174        let mut dependencies = vec![];
175        let mut stack = vec![(unit_id, 0)];
176        let graph = self.unit_graph.read();
177        while let Some((candidate_id, candidate_depth)) = stack.pop() {
178            if candidate_depth == depth {
179                // Reached the end of the search.
180                dependencies.push(candidate_id);
181                continue;
182            }
183
184            // Otherwise, look up the dependencies of the candidate and continue the search.
185            let candidate_dependencies = graph.get_dependencies(candidate_id);
186            match candidate_dependencies {
187                Some(candidate_dependencies) => {
188                    if candidate_dependencies.is_empty() {
189                        // No more dependencies to search. Add the candidate to the final list.
190                        dependencies.push(candidate_id);
191                    } else {
192                        // Continue the search with the dependencies of the candidate.
193                        stack.extend(
194                            candidate_dependencies
195                                .iter()
196                                .copied()
197                                .map(|dependency| (dependency, candidate_depth + 1)),
198                        );
199                    }
200                }
201                None => dependencies.push(candidate_id),
202            }
203        }
204
205        // Remove any units not found in the graph. This can happen if a unit claims a dependency on
206        // a unit not found in the graph.
207        dependencies.retain(|dependency| graph.get_unit_type(*dependency).is_some());
208        dependencies
209    }
210
211    /// Returns the value of the `course_id` field in the manifest of the given lesson.
212    #[inline]
213    #[must_use]
214    pub fn get_lesson_course(&self, lesson_id: Ustr) -> Option<Ustr> {
215        self.unit_graph.read().get_lesson_course(lesson_id)
216    }
217
218    /// Returns whether the unit exists in the library. Some units will exist in the unit graph
219    /// because they are a dependency of another, but their data might not exist in the library.
220    #[inline]
221    pub fn unit_exists(&self, unit_id: Ustr) -> Result<bool> {
222        // Retrieve the unit type. A missing unit type indicates the unit does not exist.
223        let unit_type = self.unit_graph.read().get_unit_type(unit_id);
224        if unit_type.is_none() {
225            return Ok(false);
226        }
227
228        // Decide whether the unit exists by looking for its manifest.
229        let library = self.course_library.read();
230        match unit_type.unwrap() {
231            UnitType::Course => Ok(library.get_course_manifest(unit_id).is_some()),
232            UnitType::Lesson => Ok(library.get_lesson_manifest(unit_id).is_some()),
233            UnitType::Exercise => Ok(library.get_exercise_manifest(unit_id).is_some()),
234        }
235    }
236
237    /// Returns the exercises contained within the given unit.
238    #[inline]
239    #[must_use]
240    pub fn get_lesson_exercises(&self, unit_id: Ustr) -> Vec<Ustr> {
241        self.unit_graph
242            .read()
243            .get_lesson_exercises(unit_id)
244            .unwrap_or_default()
245            .iter()
246            .copied()
247            .collect()
248    }
249
250    /// Returns the number of lessons in the given course.
251    #[inline]
252    #[must_use]
253    pub fn get_num_lessons_in_course(&self, course_id: Ustr) -> usize {
254        self.unit_graph
255            .read()
256            .get_course_lessons(course_id)
257            .unwrap_or_default()
258            .len()
259    }
260
261    /// Returns whether the unit passes the metadata filter, handling all interactions between
262    /// lessons and course metadata filters.
263    #[inline]
264    pub fn unit_passes_filter(
265        &self,
266        unit_id: Ustr,
267        metadata_filter: Option<&KeyValueFilter>,
268    ) -> Result<bool> {
269        // All units pass if there is no filter.
270        if metadata_filter.is_none() {
271            return Ok(true);
272        }
273
274        // Decide how to handle the filter based on the unit type.
275        let unit_type = self.get_unit_type_strict(unit_id)?;
276        match unit_type {
277            // Exercises do not have metadata, so this operation is not supported.
278            UnitType::Exercise => Err(anyhow!(
279                "cannot apply metadata filter to exercise with ID {unit_id}",
280            )),
281            UnitType::Course => {
282                // Retrieve the course manifest and check if the course passes the filter.
283                let course_manifest = self.get_course_manifest(unit_id)?;
284                Ok(metadata_filter
285                    .as_ref()
286                    .unwrap()
287                    .apply_to_course(&*course_manifest))
288            }
289            UnitType::Lesson => {
290                // Retrieve the lesson and course manifests and check if the lesson passes the
291                // filter.
292                let course_manifest =
293                    self.get_course_manifest(self.get_lesson_course(unit_id).unwrap_or_default())?;
294                let lesson_manifest = self.get_lesson_manifest(unit_id)?;
295                Ok(metadata_filter
296                    .as_ref()
297                    .unwrap()
298                    .apply_to_lesson(&*course_manifest, &*lesson_manifest))
299            }
300        }
301    }
302
303    /// Increments the value in the frequency map for the given exercise ID.
304    #[inline]
305    pub fn increment_exercise_frequency(&self, exercise_id: Ustr) {
306        let mut frequency_map = self.frequency_map.write();
307        let frequency = frequency_map.entry(exercise_id).or_insert(0);
308        *frequency += 1;
309    }
310
311    /// Returns the frequency of the given exercise ID.
312    #[inline]
313    #[must_use]
314    pub fn get_exercise_frequency(&self, exercise_id: Ustr) -> usize {
315        self.frequency_map
316            .read()
317            .get(&exercise_id)
318            .copied()
319            .unwrap_or(0)
320    }
321
322    /// Returns the unit filter for the saved filter with the given ID. Returns an error if no
323    /// filter exists with that ID exists.
324    pub fn get_saved_filter(&self, filter_id: &str) -> Result<Arc<SavedFilter>> {
325        match self.filter_manager.read().get_filter(filter_id) {
326            Some(filter) => Ok(filter),
327            None => Err(anyhow!("no saved filter with ID {filter_id} exists")),
328        }
329    }
330
331    /// Returns the unit filter that should be used for the given study session.
332    pub fn get_session_filter(
333        &self,
334        session_data: &StudySessionData,
335        time: DateTime<Utc>,
336    ) -> Result<Option<UnitFilter>> {
337        match session_data.get_part(time) {
338            SessionPart::NoFilter { .. } => Ok(None),
339            SessionPart::UnitFilter { filter, .. } => Ok(Some(filter)),
340            SessionPart::SavedFilter { filter_id, .. } => {
341                let saved_filter = self.get_saved_filter(&filter_id)?;
342                Ok(Some(saved_filter.filter.clone()))
343            }
344        }
345    }
346
347    /// Returns all the valid exercises in the given lesson.
348    #[must_use]
349    pub fn all_valid_exercises_in_lesson(&self, lesson_id: Ustr) -> Vec<Ustr> {
350        // If the lesson is blacklisted, return no exercises.
351        if self.blacklisted(lesson_id).unwrap_or(false) {
352            return vec![];
353        }
354
355        // If the course to which the lesson belongs is blacklisted, return no exercises.
356        let course_id = self.get_lesson_course(lesson_id).unwrap_or_default();
357        if self.blacklisted(course_id).unwrap_or(false) {
358            return vec![];
359        }
360
361        // Get all exercises in the lesson and filter out the blacklisted ones.
362        let exercises = self.get_lesson_exercises(lesson_id);
363        exercises
364            .into_iter()
365            .filter(|exercise_id| !self.blacklisted(*exercise_id).unwrap_or(false))
366            .collect()
367    }
368
369    /// Returns all the valid exercises in the given unit.
370    #[must_use]
371    pub fn all_valid_exercises(&self, unit_id: Ustr) -> Vec<Ustr> {
372        // First, get the type of the unit. Then get the exercises based on the unit type.
373        let unit_type = self.get_unit_type(unit_id);
374        match unit_type {
375            None => vec![],
376            Some(UnitType::Exercise) => {
377                // Return the exercise if it's not blacklisted.
378                if self.blacklisted(unit_id).unwrap_or(false) {
379                    vec![]
380                } else {
381                    vec![unit_id]
382                }
383            }
384            Some(UnitType::Lesson) => self.all_valid_exercises_in_lesson(unit_id),
385            Some(UnitType::Course) => {
386                // If the course is blacklisted, return no exercises.
387                if self.blacklisted(unit_id).unwrap_or(false) {
388                    return vec![];
389                }
390
391                // Otherwise, get all the lessons and the valid exercises in each.
392                let lessons = self
393                    .unit_graph
394                    .read()
395                    .get_course_lessons(unit_id)
396                    .unwrap_or_default();
397                lessons
398                    .iter()
399                    .copied()
400                    .flat_map(|lesson_id| self.all_valid_exercises_in_lesson(lesson_id))
401                    .collect()
402            }
403        }
404    }
405
406    /// Update the count of successful and failed exercises based on the given score.
407    pub fn update_success_rate(&self, score: &MasteryScore) {
408        let mut counts = self.trial_counts.write();
409        match score {
410            MasteryScore::One | MasteryScore::Two => counts.1 += 1,
411            MasteryScore::Three | MasteryScore::Four | MasteryScore::Five => counts.0 += 1,
412        }
413    }
414
415    /// Returns the success rate of the current session.
416    #[must_use]
417    pub fn get_success_rate(&self) -> f32 {
418        let counts = self.trial_counts.read();
419        let total = counts.0 + counts.1;
420        if total == 0 {
421            1.0
422        } else {
423            counts.0 as f32 / total as f32
424        }
425    }
426}
427
428#[cfg(test)]
429#[cfg_attr(coverage, coverage(off))]
430mod test {
431    use anyhow::Result;
432    use chrono::Duration;
433    use parking_lot::RwLock;
434    use std::{
435        collections::{BTreeMap, HashMap},
436        sync::{Arc, LazyLock},
437    };
438    use ustr::Ustr;
439
440    use crate::{
441        data::{
442            MasteryScore, UnitType,
443            filter::{
444                FilterType, KeyValueFilter, SavedFilter, SessionPart, StudySession,
445                StudySessionData, UnitFilter,
446            },
447        },
448        filter_manager::LocalFilterManager,
449        test_utils::*,
450    };
451
452    static NUM_EXERCISES: usize = 2;
453
454    /// A simple set of courses to test the basic functionality of Trane.
455    static TEST_LIBRARY: LazyLock<Vec<TestCourse>> = LazyLock::new(|| {
456        vec![TestCourse {
457            id: TestId(0, None, None),
458            dependencies: vec![],
459            encompassed: vec![],
460            superseded: vec![],
461            metadata: BTreeMap::from([
462                (
463                    "course_key_1".to_string(),
464                    vec!["course_key_1:value_1".to_string()],
465                ),
466                (
467                    "course_key_2".to_string(),
468                    vec!["course_key_2:value_1".to_string()],
469                ),
470            ]),
471            lessons: vec![
472                TestLesson {
473                    id: TestId(0, Some(0), None),
474                    dependencies: vec![],
475                    encompassed: vec![],
476                    superseded: vec![],
477                    metadata: BTreeMap::from([
478                        (
479                            "lesson_key_1".to_string(),
480                            vec!["lesson_key_1:value_1".to_string()],
481                        ),
482                        (
483                            "lesson_key_2".to_string(),
484                            vec!["lesson_key_2:value_1".to_string()],
485                        ),
486                    ]),
487                    num_exercises: NUM_EXERCISES,
488                },
489                TestLesson {
490                    id: TestId(0, Some(1), None),
491                    dependencies: vec![TestId(0, Some(0), None)],
492                    encompassed: vec![],
493                    superseded: vec![],
494                    metadata: BTreeMap::from([
495                        (
496                            "lesson_key_1".to_string(),
497                            vec!["lesson_key_1:value_2".to_string()],
498                        ),
499                        (
500                            "lesson_key_2".to_string(),
501                            vec!["lesson_key_2:value_2".to_string()],
502                        ),
503                    ]),
504                    num_exercises: NUM_EXERCISES,
505                },
506            ],
507        }]
508    });
509
510    /// Verifies that the scheduler data correctly knows which units exist and their types.
511    #[test]
512    fn unit_exists() -> Result<()> {
513        let temp_dir = tempfile::tempdir()?;
514        let library = init_test_simulation(temp_dir.path(), &TEST_LIBRARY)?;
515        let scheduler_data = library.get_scheduler_data();
516
517        assert_eq!(
518            scheduler_data.get_unit_type_strict(Ustr::from("0"))?,
519            UnitType::Course
520        );
521        assert!(scheduler_data.unit_exists(Ustr::from("0"))?);
522        assert_eq!(
523            scheduler_data.get_unit_type_strict(Ustr::from("0::0"))?,
524            UnitType::Lesson
525        );
526        assert!(scheduler_data.unit_exists(Ustr::from("0::0"))?);
527        assert_eq!(
528            scheduler_data.get_unit_type_strict(Ustr::from("0::0::0"))?,
529            UnitType::Exercise
530        );
531        assert!(scheduler_data.unit_exists(Ustr::from("0::0::0"))?);
532        Ok(())
533    }
534
535    /// Verifies that a metadata filter cannot be applied to an exercise.
536    #[test]
537    fn exercise_metadata_filter() -> Result<()> {
538        let temp_dir = tempfile::tempdir()?;
539        let library = init_test_simulation(temp_dir.path(), &TEST_LIBRARY)?;
540        let scheduler_data = library.get_scheduler_data();
541        let metadata_filter = KeyValueFilter::CourseFilter {
542            key: "key".into(),
543            value: "value".into(),
544            filter_type: FilterType::Include,
545        };
546        assert!(
547            scheduler_data
548                .unit_passes_filter(Ustr::from("0::0::0"), Some(&metadata_filter))
549                .is_err()
550        );
551        Ok(())
552    }
553
554    /// Verifies that the frequency of an exercise is correctly incremented when the exercise is
555    /// scheduled.
556    #[test]
557    fn exercise_frequency() -> Result<()> {
558        let temp_dir = tempfile::tempdir()?;
559        let library = init_test_simulation(temp_dir.path(), &TEST_LIBRARY)?;
560        let scheduler_data = library.get_scheduler_data();
561
562        assert_eq!(
563            scheduler_data.get_exercise_frequency(Ustr::from("0::0::0")),
564            0
565        );
566        scheduler_data.increment_exercise_frequency(Ustr::from("0::0::0"));
567        assert_eq!(
568            scheduler_data.get_exercise_frequency(Ustr::from("0::0::0")),
569            1
570        );
571        Ok(())
572    }
573
574    /// Verifies retrieving the filter for a session part.
575    #[test]
576    fn get_session_filter() -> Result<()> {
577        let temp_dir = tempfile::tempdir()?;
578        let library = init_test_simulation(temp_dir.path(), &TEST_LIBRARY)?;
579
580        // Add a saved filter to the filter manager.
581        let mut scheduler_data = library.get_scheduler_data();
582        scheduler_data.filter_manager = Arc::new(RwLock::new(LocalFilterManager {
583            filters: HashMap::from([(
584                "saved_filter".to_string(),
585                Arc::new(SavedFilter {
586                    id: "saved_filter".to_string(),
587                    description: "Saved filter".to_string(),
588                    filter: UnitFilter::ReviewListFilter,
589                }),
590            )]),
591        }));
592
593        // Define the data for the study session.
594        let start_time = chrono::Utc::now();
595        let session_data = StudySessionData {
596            start_time,
597            definition: StudySession {
598                id: "session".to_string(),
599                description: "Session".to_string(),
600                parts: vec![
601                    SessionPart::UnitFilter {
602                        filter: UnitFilter::ReviewListFilter,
603                        duration: 1,
604                    },
605                    SessionPart::NoFilter { duration: 1 },
606                    SessionPart::SavedFilter {
607                        filter_id: "saved_filter".into(),
608                        duration: 1,
609                    },
610                ],
611            },
612        };
613
614        // Verify that the filter for each session part is correct.
615        assert_eq!(
616            scheduler_data.get_session_filter(&session_data, start_time)?,
617            Some(UnitFilter::ReviewListFilter)
618        );
619        assert_eq!(
620            scheduler_data.get_session_filter(&session_data, start_time + Duration::minutes(1))?,
621            None
622        );
623        assert_eq!(
624            scheduler_data.get_session_filter(&session_data, start_time + Duration::minutes(2))?,
625            Some(UnitFilter::ReviewListFilter)
626        );
627
628        // Verify that trying to retrieve an unknown saved filter returns an error.
629        assert!(
630            scheduler_data
631                .get_session_filter(
632                    &StudySessionData {
633                        start_time,
634                        definition: StudySession {
635                            id: "session".to_string(),
636                            description: "Session".to_string(),
637                            parts: vec![SessionPart::SavedFilter {
638                                filter_id: "unknown_filter".into(),
639                                duration: 1,
640                            }],
641                        },
642                    },
643                    start_time
644                )
645                .is_err()
646        );
647
648        Ok(())
649    }
650
651    /// Verifies retrieving the valid exercises from a unit.
652    #[test]
653    fn all_valid_exercises() -> Result<()> {
654        // Generate a test library.
655        let temp_dir = tempfile::tempdir()?;
656        let library = init_test_simulation(temp_dir.path(), &TEST_LIBRARY)?;
657        let scheduler_data = library.get_scheduler_data();
658
659        // Verify an empty list is returned when an unknown unit is passed.
660        assert!(
661            scheduler_data
662                .all_valid_exercises(Ustr::from("unknown"))
663                .is_empty()
664        );
665
666        // Get the valid exercises when the ID is an exercise.
667        assert_eq!(
668            scheduler_data.all_valid_exercises(Ustr::from("0::0::0")),
669            vec![Ustr::from("0::0::0")]
670        );
671
672        // Blacklist that exercise and verify it's no longer valid.
673        scheduler_data
674            .blacklist
675            .write()
676            .add_to_blacklist(Ustr::from("0::0::0"))?;
677        assert!(
678            scheduler_data
679                .all_valid_exercises(Ustr::from("0::0::0"))
680                .is_empty()
681        );
682
683        // Get the valid exercises when the ID is a lesson.
684        let mut valid_exercises = scheduler_data.all_valid_exercises(Ustr::from("0::1"));
685        valid_exercises.sort();
686        assert_eq!(
687            valid_exercises,
688            vec![Ustr::from("0::1::0"), Ustr::from("0::1::1")]
689        );
690
691        // Blacklist the lesson and verify the exercises are no longer valid.
692        scheduler_data
693            .blacklist
694            .write()
695            .add_to_blacklist(Ustr::from("0::1"))?;
696        assert!(
697            scheduler_data
698                .all_valid_exercises(Ustr::from("0::1"))
699                .is_empty()
700        );
701
702        // Get the valid exercises when the ID is a course.
703        assert_eq!(
704            scheduler_data.all_valid_exercises(Ustr::from("0")),
705            vec![Ustr::from("0::0::1"),]
706        );
707
708        // Blacklist the course and verify the exercises are no longer valid.
709        scheduler_data
710            .blacklist
711            .write()
712            .add_to_blacklist(Ustr::from("0"))?;
713        assert!(
714            scheduler_data
715                .all_valid_exercises(Ustr::from("0"))
716                .is_empty()
717        );
718
719        Ok(())
720    }
721
722    /// Verifies that the success rate is correctly updated and retrieved.
723    #[test]
724    fn success_rate() -> Result<()> {
725        let temp_dir = tempfile::tempdir()?;
726        let library = init_test_simulation(temp_dir.path(), &TEST_LIBRARY)?;
727        let scheduler_data = library.get_scheduler_data();
728
729        // Verify the initial success rate is 1.0.
730        assert_eq!(scheduler_data.get_success_rate(), 1.0);
731
732        // Add some successes and failures and verify the success rate. Score 3 counts as
733        // success, so One and Two are failures, Three, Four, and Five are successes.
734        scheduler_data.update_success_rate(&MasteryScore::One);
735        scheduler_data.update_success_rate(&MasteryScore::Two);
736        scheduler_data.update_success_rate(&MasteryScore::Three);
737        scheduler_data.update_success_rate(&MasteryScore::Four);
738        scheduler_data.update_success_rate(&MasteryScore::Five);
739        assert_eq!(scheduler_data.get_success_rate(), 0.6);
740        Ok(())
741    }
742}