use anyhow::{Ok, Result};
use std::{collections::BTreeMap, sync::LazyLock};
use tempfile::TempDir;
use trane::{
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![TestId(0, None, None)],
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![],
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![TestId(2, Some(0), None)],
metadata: BTreeMap::default(),
num_exercises: 10,
},
],
},
TestCourse {
id: TestId(3, None, None),
dependencies: vec![],
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,
},
],
},
TestCourse {
id: TestId(4, None, None),
dependencies: vec![TestId(3, None, None)],
encompassed: vec![],
superseded: vec![TestId(3, None, None)],
metadata: BTreeMap::default(),
lessons: vec![
TestLesson {
id: TestId(4, Some(0), None),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(4, Some(1), None),
dependencies: vec![TestId(4, Some(0), None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
],
},
TestCourse {
id: TestId(5, None, None),
dependencies: vec![TestId(4, None, None)],
encompassed: vec![],
superseded: vec![TestId(4, None, None)],
metadata: BTreeMap::default(),
lessons: vec![
TestLesson {
id: TestId(5, Some(0), None),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(5, Some(1), None),
dependencies: vec![TestId(5, Some(0), None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
],
},
TestCourse {
id: TestId(6, None, None),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
lessons: vec![
TestLesson {
id: TestId(6, Some(0), None),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(6, Some(1), None),
dependencies: vec![TestId(6, Some(0), None)],
encompassed: vec![],
superseded: vec![TestId(6, Some(0), None)],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(6, Some(2), None),
dependencies: vec![TestId(6, Some(1), None)],
encompassed: vec![],
superseded: vec![TestId(6, Some(1), None)],
metadata: BTreeMap::default(),
num_exercises: 10,
},
],
},
TestCourse {
id: TestId(7, None, None),
dependencies: vec![TestId(6, None, None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
lessons: vec![TestLesson {
id: TestId(7, Some(0), None),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
}],
},
]
});
#[test]
fn scheduler_respects_superseded_courses() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let superseded_course_id = TestId(0, None, None);
let mut simulation = TraneSimulation::new(2000, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(&mut trane, &vec![], &None)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
assert!(
simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should have been scheduled",
exercise_id
);
assert_simulation_scores(exercise_ustr, &trane, &simulation.answer_history)?;
}
let mut simulation = TraneSimulation::new(2000, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(&mut trane, &vec![], &None)?;
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if exercise_id.exercise_in_course(&superseded_course_id) {
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
}
}
let mut simulation = TraneSimulation::new(
2000,
Box::new(|id| {
if id.starts_with("1::") {
Some(MasteryScore::One)
} else {
Some(MasteryScore::Five)
}
}),
);
simulation.run_simulation(&mut trane, &vec![], &None)?;
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if exercise_id.exercise_in_course(&superseded_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)?;
}
}
Ok(())
}
#[test]
fn scheduler_respects_superseded_lessons() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let superseded_lesson_id = TestId(2, Some(0), None);
let mut simulation = TraneSimulation::new(2500, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(&mut trane, &vec![], &None)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
assert!(
simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should have been scheduled",
exercise_id
);
assert_simulation_scores(exercise_ustr, &trane, &simulation.answer_history)?;
}
let mut simulation = TraneSimulation::new(2500, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(&mut trane, &vec![], &None)?;
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if exercise_id.exercise_in_lesson(&superseded_lesson_id) {
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
}
}
let mut simulation = TraneSimulation::new(
2500,
Box::new(|id| {
if id.starts_with("2::2::") {
Some(MasteryScore::One)
} else {
Some(MasteryScore::Five)
}
}),
);
simulation.run_simulation(&mut trane, &vec![], &None)?;
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if exercise_id.exercise_in_course(&superseded_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)?;
}
}
Ok(())
}
#[test]
fn scheduler_respects_superseded_course_chain() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let superseded_course_ids = [TestId(3, None, None), TestId(4, None, None)];
let mut simulation = TraneSimulation::new(2000, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(&mut trane, &vec![], &None)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
assert!(
simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should have been scheduled",
exercise_id
);
assert_simulation_scores(exercise_ustr, &trane, &simulation.answer_history)?;
}
let mut simulation = TraneSimulation::new(2000, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(&mut trane, &vec![], &None)?;
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if superseded_course_ids
.iter()
.any(|id| exercise_id.exercise_in_course(id))
{
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
}
}
let mut simulation = TraneSimulation::new(
2000,
Box::new(|id| {
if id.starts_with("5::") {
Some(MasteryScore::One)
} else {
Some(MasteryScore::Five)
}
}),
);
simulation.run_simulation(&mut trane, &vec![], &None)?;
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if exercise_id.exercise_in_course(&superseded_course_ids[1]) {
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 if exercise_id.exercise_in_course(&superseded_course_ids[0]) {
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
}
}
let mut simulation = TraneSimulation::new(
2000,
Box::new(|id| {
if id.starts_with("4::") || id.starts_with("5::") {
Some(MasteryScore::One)
} else {
Some(MasteryScore::Five)
}
}),
);
simulation.run_simulation(&mut trane, &vec![], &None)?;
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if exercise_id.exercise_in_course(&superseded_course_ids[0]) {
assert!(
simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should have been scheduled",
exercise_id
);
assert_simulation_scores(exercise_ustr, &trane, &simulation.answer_history)?;
}
}
Ok(())
}
#[test]
fn scheduler_respects_superseded_lesson_chain() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let superseded_lesson_ids = [TestId(6, Some(0), None), TestId(6, Some(1), None)];
let mut simulation = TraneSimulation::new(2000, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(&mut trane, &vec![], &None)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
assert!(
simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should have been scheduled",
exercise_id
);
assert_simulation_scores(exercise_ustr, &trane, &simulation.answer_history)?;
}
let mut simulation = TraneSimulation::new(2000, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(&mut trane, &vec![], &None)?;
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if superseded_lesson_ids
.iter()
.any(|id| exercise_id.exercise_in_lesson(id))
{
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
}
}
let mut simulation = TraneSimulation::new(
2000,
Box::new(|id| {
if id.starts_with("6::2::") {
Some(MasteryScore::One)
} else {
Some(MasteryScore::Five)
}
}),
);
simulation.run_simulation(&mut trane, &vec![], &None)?;
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if exercise_id.exercise_in_lesson(&superseded_lesson_ids[1]) {
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 if exercise_id.exercise_in_lesson(&superseded_lesson_ids[0]) {
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
}
}
let mut simulation = TraneSimulation::new(
2000,
Box::new(|id| {
if id.starts_with("6::1") || id.starts_with("6::2") {
Some(MasteryScore::One)
} else {
Some(MasteryScore::Five)
}
}),
);
simulation.run_simulation(&mut trane, &vec![], &None)?;
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if exercise_id.exercise_in_lesson(&superseded_lesson_ids[0]) {
assert!(
simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should have been scheduled",
exercise_id
);
assert_simulation_scores(exercise_ustr, &trane, &simulation.answer_history)?;
}
}
Ok(())
}
#[test]
fn scheduler_ignores_superseded_exercises() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let superseded_lesson_ids = [TestId(6, Some(0), None), TestId(6, Some(1), None)];
let mut simulation = TraneSimulation::new(2000, Box::new(|_| Some(MasteryScore::One)));
simulation.run_simulation(
&mut trane,
&vec![],
&Some(ExerciseFilter::UnitFilter(UnitFilter::LessonFilter {
lesson_ids: superseded_lesson_ids
.iter()
.map(|id| id.to_ustr())
.collect(),
})),
)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if superseded_lesson_ids
.iter()
.any(|id| exercise_id.exercise_in_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)?;
}
}
let superseding_lesson_ids = [TestId(6, Some(2), None)];
let mut simulation = TraneSimulation::new(2000, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(
&mut trane,
&vec![],
&Some(ExerciseFilter::UnitFilter(UnitFilter::LessonFilter {
lesson_ids: superseding_lesson_ids
.iter()
.map(|id| id.to_ustr())
.collect(),
})),
)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if superseding_lesson_ids
.iter()
.any(|id| exercise_id.exercise_in_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)?;
}
}
let mut simulation = TraneSimulation::new(2000, Box::new(|_| Some(MasteryScore::Five)));
let dependant_course = TestId(7, None, None);
simulation.run_simulation(&mut trane, &vec![], &None)?;
for exercise_id in &exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if exercise_id.exercise_in_course(&dependant_course) {
assert!(
simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should have been scheduled",
exercise_id
);
assert_simulation_scores(exercise_ustr, &trane, &simulation.answer_history)?;
}
}
Ok(())
}