use anyhow::{Ok, Result};
use std::{collections::BTreeMap, sync::LazyLock};
use tempfile::TempDir;
use trane::{
blacklist::Blacklist,
data::{
MasteryScore,
filter::{ExerciseFilter, UnitFilter},
},
test_utils::*,
};
static LIBRARY: LazyLock<Vec<TestCourse>> = LazyLock::new(|| {
vec![
TestCourse {
id: TestId(0, None, None),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
lessons: vec![
TestLesson {
id: TestId(0, Some(0), None),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(0, Some(1), None),
dependencies: vec![TestId(0, Some(0), None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
],
},
TestCourse {
id: TestId(1, None, None),
dependencies: vec![TestId(0, None, None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
lessons: vec![
TestLesson {
id: TestId(1, Some(0), None),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(1, Some(1), None),
dependencies: vec![TestId(1, Some(0), None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
],
},
TestCourse {
id: TestId(2, None, None),
dependencies: vec![TestId(0, None, None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
lessons: vec![
TestLesson {
id: TestId(2, Some(0), None),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(2, Some(1), None),
dependencies: vec![TestId(2, Some(0), None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(2, Some(2), None),
dependencies: vec![TestId(2, Some(1), None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
],
},
TestCourse {
id: TestId(3, None, None),
dependencies: vec![TestId(1, None, None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
lessons: vec![
TestLesson {
id: TestId(3, Some(0), None),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(3, Some(1), None),
dependencies: vec![TestId(3, Some(0), None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
],
},
]
});
#[test]
fn avoid_scheduling_courses_in_blacklist() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let mut simulation = TraneSimulation::new(500, Box::new(|_| Some(MasteryScore::Five)));
let course_blacklist = vec![TestId(0, None, None), TestId(3, None, None)];
simulation.run_simulation(&mut trane, &course_blacklist, &None)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if !course_blacklist
.iter()
.any(|course_id| exercise_id.exercise_in_course(course_id))
{
assert!(
simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should have been scheduled",
exercise_id
);
assert_simulation_scores(exercise_ustr, &trane, &simulation.answer_history)?;
} else {
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
}
}
Ok(())
}
#[test]
fn avoid_scheduling_lessons_in_blacklist() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let mut simulation = TraneSimulation::new(500, Box::new(|_| Some(MasteryScore::Five)));
let lesson_blacklist = vec![TestId(0, Some(1), None), TestId(3, Some(0), None)];
simulation.run_simulation(&mut trane, &lesson_blacklist, &None)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if !lesson_blacklist
.iter()
.any(|lesson_id| exercise_id.exercise_in_lesson(lesson_id))
{
assert!(
simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should have been scheduled",
exercise_id
);
assert_simulation_scores(exercise_ustr, &trane, &simulation.answer_history)?;
} else {
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
}
}
Ok(())
}
#[test]
fn avoid_scheduling_lessons_in_blacklist_with_course_filter() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let lesson_blacklist = vec![TestId(0, Some(1), None), TestId(3, Some(0), None)];
let selected_courses = [TestId(0, None, None), TestId(3, None, None)];
let course_filter = UnitFilter::CourseFilter {
course_ids: selected_courses.iter().map(|id| id.to_ustr()).collect(),
};
let mut simulation = TraneSimulation::new(500, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(
&mut trane,
&lesson_blacklist,
&Some(ExerciseFilter::UnitFilter(course_filter)),
)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
let in_blacklisted_lesson = lesson_blacklist
.iter()
.any(|lesson_id| exercise_id.exercise_in_lesson(lesson_id));
let in_selected_course = selected_courses
.iter()
.any(|course_id| exercise_id.exercise_in_course(course_id));
if in_selected_course && !in_blacklisted_lesson {
assert!(
simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should have been scheduled",
exercise_id
);
assert_simulation_scores(exercise_ustr, &trane, &simulation.answer_history)?;
} else {
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
}
}
Ok(())
}
#[test]
fn avoid_scheduling_exercises_in_blacklist() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let mut simulation = TraneSimulation::new(500, Box::new(|_| Some(MasteryScore::Five)));
let exercise_blacklist = vec![
TestId(2, Some(1), Some(0)),
TestId(2, Some(1), Some(1)),
TestId(2, Some(1), Some(2)),
TestId(2, Some(1), Some(3)),
TestId(2, Some(1), Some(4)),
TestId(2, Some(1), Some(5)),
TestId(2, Some(1), Some(6)),
TestId(2, Some(1), Some(7)),
TestId(2, Some(1), Some(8)),
TestId(2, Some(1), Some(9)),
];
simulation.run_simulation(&mut trane, &exercise_blacklist, &None)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if !exercise_blacklist
.iter()
.any(|blacklisted_id| *blacklisted_id == exercise_id)
{
assert!(
simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should have been scheduled",
exercise_id
);
assert_simulation_scores(exercise_ustr, &trane, &simulation.answer_history)?;
} else {
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
}
}
Ok(())
}
#[test]
fn invalidate_cache_on_blacklist_update() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let exercise_blacklist = vec![
TestId(0, Some(0), Some(0)),
TestId(0, Some(0), Some(1)),
TestId(0, Some(0), Some(2)),
TestId(0, Some(0), Some(3)),
TestId(0, Some(0), Some(4)),
TestId(0, Some(0), Some(5)),
TestId(0, Some(0), Some(6)),
TestId(0, Some(0), Some(7)),
TestId(0, Some(0), Some(8)),
TestId(0, Some(0), Some(9)),
TestId(0, Some(1), Some(0)),
TestId(0, Some(1), Some(1)),
TestId(0, Some(1), Some(2)),
TestId(0, Some(1), Some(3)),
TestId(0, Some(1), Some(4)),
TestId(0, Some(1), Some(5)),
TestId(0, Some(1), Some(6)),
TestId(0, Some(1), Some(7)),
TestId(0, Some(1), Some(8)),
TestId(0, Some(1), Some(9)),
];
let mut simulation = TraneSimulation::new(500, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(&mut trane, &exercise_blacklist, &None)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in &exercise_ids {
if exercise_blacklist
.iter()
.any(|blacklisted_id| *blacklisted_id == *exercise_id)
{
let exercise_ustr = exercise_id.to_ustr();
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
} else {
assert!(
simulation
.answer_history
.contains_key(&exercise_id.to_ustr()),
"exercise {:?} should have been scheduled",
exercise_id
);
}
}
for exercise_id in &exercise_blacklist {
trane.remove_from_blacklist(exercise_id.to_ustr())?;
}
let mut simulation = TraneSimulation::new(500, Box::new(|_| Some(MasteryScore::One)));
simulation.run_simulation(&mut trane, &vec![], &None)?;
let unscheduled_lessons = vec![
TestId(0, Some(1), None),
TestId(1, Some(0), None),
TestId(1, Some(1), None),
TestId(2, Some(0), None),
TestId(2, Some(1), None),
TestId(2, Some(2), None),
TestId(3, Some(0), None),
TestId(3, Some(1), None),
];
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if exercise_id.exercise_in_lesson(&TestId(0, Some(0), None)) {
assert!(
simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should have been scheduled",
exercise_id
);
} else if unscheduled_lessons
.iter()
.any(|lesson_id| exercise_id.exercise_in_lesson(lesson_id))
{
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
}
}
let mut simulation = TraneSimulation::new(500, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(&mut trane, &exercise_blacklist, &None)?;
for exercise_id in &exercise_blacklist {
let exercise_ustr = exercise_id.to_ustr();
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
}
Ok(())
}