use std::{collections::BTreeMap, sync::LazyLock};
use anyhow::{Ok, Result};
use chrono::{Duration, Utc};
use tempfile::TempDir;
use trane::{
Trane,
course_library::{CourseLibrary, LocalCourseLibrary, SerializedCourseLibrary},
data::{
MasteryScore, SchedulerOptions, UnitType, UserPreferences,
filter::{ExerciseFilter, SessionPart, StudySession, StudySessionData, UnitFilter},
},
review_list::ReviewList,
scheduler::ExerciseScheduler,
test_utils::*,
};
use ustr::Ustr;
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(4, None, None),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
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), TestId(2, Some(1), None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(4, Some(2), None),
dependencies: vec![TestId(4, Some(0), None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(4, Some(3), None),
dependencies: vec![TestId(4, Some(2), None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
],
},
TestCourse {
id: TestId(5, None, None),
dependencies: vec![
TestId(3, None, None),
TestId(4, None, None),
],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
lessons: vec![
TestLesson {
id: TestId(5, Some(0), None),
dependencies: vec![TestId(4, Some(1), None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(5, Some(1), None),
dependencies: vec![
TestId(5, Some(0), None),
TestId(3, Some(3), None),
],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
],
},
TestCourse {
id: TestId(6, None, None),
dependencies: vec![TestId(3, None, None)],
encompassed: vec![(TestId(5, Some(0), None), 0.5)],
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![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
],
},
TestCourse {
id: TestId(7, None, None),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::from([
(
"course_key_1".to_string(),
vec!["course_key_1:value_1".to_string()],
),
(
"course_key_2".to_string(),
vec!["course_key_2:value_1".to_string()],
),
]),
lessons: vec![
TestLesson {
id: TestId(7, Some(0), None),
dependencies: vec![TestId(0, None, None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(7, Some(1), None),
dependencies: vec![
TestId(0, Some(0), None),
TestId(6, Some(11), None),
],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 10,
},
TestLesson {
id: TestId(7, Some(2), None),
dependencies: vec![TestId(7, Some(1), None)],
encompassed: vec![(TestId(7, Some(0), None), 1.0)],
superseded: vec![],
metadata: BTreeMap::default(),
num_exercises: 0,
},
],
},
TestCourse {
id: TestId(8, None, None),
dependencies: vec![TestId(7, None, None)],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
lessons: vec![],
},
TestCourse {
id: TestId(9, None, None),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
metadata: BTreeMap::default(),
lessons: vec![],
},
]
});
#[test]
fn get_unit_ids() -> Result<()> {
let temp_dir = TempDir::new()?;
let trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let course_ids = trane.get_course_ids();
let expected_course_ids = vec![
Ustr::from("0"),
Ustr::from("1"),
Ustr::from("2"),
Ustr::from("4"),
Ustr::from("5"),
Ustr::from("6"),
Ustr::from("7"),
Ustr::from("8"),
Ustr::from("9"),
];
assert_eq!(course_ids, expected_course_ids);
for course_id in course_ids {
let course_test_id = TestId::from(&course_id);
let lesson_ids = trane.get_lesson_ids(course_id).unwrap_or_default();
for lesson_id in lesson_ids {
let lesson_test_id = TestId::from(&lesson_id);
assert_eq!(course_test_id.0, lesson_test_id.0);
assert_eq!(lesson_test_id.2, None);
let exercise_ids = trane.get_exercise_ids(lesson_id).unwrap_or_default();
for exercise_id in exercise_ids {
let exercise_test_id = TestId::from(&exercise_id);
assert!(exercise_test_id.2.is_some());
assert_eq!(course_test_id.0, exercise_test_id.0);
assert_eq!(lesson_test_id.1, exercise_test_id.1);
}
}
}
Ok(())
}
#[test]
fn get_all_exercise_ids() -> Result<()> {
let temp_dir = TempDir::new()?;
let trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let exercise_ids = trane.get_all_exercise_ids(Some(Ustr::from("0")));
assert!(!exercise_ids.is_empty());
for exercise_id in exercise_ids {
assert!(exercise_id.starts_with("0::"));
}
let exercise_ids = trane.get_all_exercise_ids(Some(Ustr::from("0::0")));
assert!(!exercise_ids.is_empty());
for exercise_id in exercise_ids {
assert!(exercise_id.starts_with("0::0::"));
}
let exercise_ids = trane.get_all_exercise_ids(Some(Ustr::from("0::0::0")));
assert_eq!(exercise_ids.len(), 1);
assert_eq!(exercise_ids[0], Ustr::from("0::0::0"));
let exercise_ids = trane.get_all_exercise_ids(Some(Ustr::from("0::0::100")));
assert!(exercise_ids.is_empty());
let exercise_ids = trane.get_all_exercise_ids(None);
let expected_ids = all_test_exercises(&LIBRARY);
assert_eq!(exercise_ids.len(), expected_ids.len());
Ok(())
}
#[test]
fn all_exercises_scheduled() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let mut simulation = TraneSimulation::new(1000, 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)?;
}
Ok(())
}
#[test]
fn bad_score_prevents_advancing() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let mut simulation = TraneSimulation::new(200, Box::new(|_| Some(MasteryScore::One)));
simulation.run_simulation(&mut trane, &vec![], &None)?;
let first_lessons = [
TestId(0, Some(0), None),
TestId(4, Some(0), None),
TestId(6, Some(0), None),
];
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if first_lessons
.iter()
.any(|lesson| exercise_id.exercise_in_lesson(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 scheduler_respects_course_filter() -> 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_filter = UnitFilter::CourseFilter {
course_ids: vec![TestId(4, None, None).to_ustr()],
};
simulation.run_simulation(
&mut trane,
&vec![],
&Some(ExerciseFilter::UnitFilter(course_filter)),
)?;
simulation.answer_history.clear();
let selected_courses = [
TestId(2, None, None),
TestId(3, None, None),
];
let course_filter = UnitFilter::CourseFilter {
course_ids: selected_courses.iter().map(|id| id.to_ustr()).collect(),
};
simulation.run_simulation(
&mut trane,
&vec![],
&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();
if selected_courses
.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
);
}
}
for exercise_id in simulation.answer_history.keys() {
let exercise_test_id = TestId::from(exercise_id);
assert!(
selected_courses
.iter()
.any(|course_id| exercise_test_id.exercise_in_course(course_id)),
"exercise {:?} should be from one of the selected courses",
exercise_id
);
}
Ok(())
}
#[test]
fn scheduler_respects_lesson_filter() -> 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 selected_lessons = [
TestId(2, Some(0), None),
TestId(4, Some(1), None),
TestId(3, Some(0), None),
];
let lesson_filter = UnitFilter::LessonFilter {
lesson_ids: selected_lessons.iter().map(|id| id.to_ustr()).collect(),
};
simulation.run_simulation(
&mut trane,
&vec![],
&Some(ExerciseFilter::UnitFilter(lesson_filter)),
)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if selected_lessons
.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 schedule_exercises_in_review_list() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let review_exercises = vec![TestId(1, Some(0), Some(0)), TestId(2, Some(1), Some(7))];
for unit_id in &review_exercises {
let unit_ustr = unit_id.to_ustr();
trane.add_to_review_list(unit_ustr)?;
}
let mut simulation = TraneSimulation::new(100, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(
&mut trane,
&vec![],
&Some(ExerciseFilter::UnitFilter(UnitFilter::ReviewListFilter)),
)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if review_exercises
.iter()
.any(|review_exercise_id| *review_exercise_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 schedule_lessons_in_review_list() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let review_lessons = vec![TestId(1, Some(0), None), TestId(2, Some(1), None)];
for unit_id in &review_lessons {
let unit_ustr = unit_id.to_ustr();
trane.add_to_review_list(unit_ustr)?;
}
let mut simulation = TraneSimulation::new(100, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(
&mut trane,
&vec![],
&Some(ExerciseFilter::UnitFilter(UnitFilter::ReviewListFilter)),
)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if review_lessons
.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 schedule_courses_in_review_list() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let review_courses = vec![TestId(1, None, None), TestId(2, None, None)];
for unit_id in &review_courses {
let unit_ustr = unit_id.to_ustr();
trane.add_to_review_list(unit_ustr)?;
}
let mut simulation = TraneSimulation::new(500, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(
&mut trane,
&vec![],
&Some(ExerciseFilter::UnitFilter(UnitFilter::ReviewListFilter)),
)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if review_courses
.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 schedule_units_and_dependents() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let starting_units = [TestId(5, Some(0), None)];
let unit_and_dependents = [
TestId(5, Some(0), None),
TestId(5, Some(1), None),
TestId(5, Some(2), None),
];
let mut simulation = TraneSimulation::new(500, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(
&mut trane,
&vec![],
&Some(ExerciseFilter::UnitFilter(UnitFilter::Dependents {
unit_ids: starting_units
.iter()
.map(|unit_id| unit_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 unit_and_dependents
.iter()
.any(|course_id| exercise_id.exercise_in_lesson(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 schedule_dependencies() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let starting_units = [TestId(5, Some(1), None)];
let depth = 1;
let matching_lessons = [TestId(5, Some(0), None), TestId(5, Some(1), None)];
let mut simulation = TraneSimulation::new(500, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(
&mut trane,
&vec![],
&Some(ExerciseFilter::UnitFilter(UnitFilter::Dependencies {
unit_ids: starting_units
.iter()
.map(|unit_id| unit_id.to_ustr())
.collect(),
depth,
})),
)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if matching_lessons
.iter()
.any(|course_id| exercise_id.exercise_in_lesson(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 schedule_dependencies_large_depth() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let starting_units = [TestId(2, None, None)];
let depth = 5;
let matching_courses = [
TestId(0, None, None),
TestId(1, None, None),
TestId(2, None, None),
TestId(7, None, None),
TestId(8, None, None),
];
let mut simulation = TraneSimulation::new(500, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(
&mut trane,
&vec![],
&Some(ExerciseFilter::UnitFilter(UnitFilter::Dependencies {
unit_ids: starting_units
.iter()
.map(|unit_id| unit_id.to_ustr())
.collect(),
depth,
})),
)?;
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if matching_courses
.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 schedule_dependencies_unknown_unit() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let starting_units = [TestId(20, None, None)];
let depth = 5;
let mut simulation = TraneSimulation::new(500, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(
&mut trane,
&vec![],
&Some(ExerciseFilter::UnitFilter(UnitFilter::Dependencies {
unit_ids: starting_units
.iter()
.map(|unit_id| unit_id.to_ustr())
.collect(),
depth,
})),
)?;
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 not have been scheduled",
exercise_id
);
}
Ok(())
}
#[test]
fn schedule_study_session() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let session_data = StudySessionData {
start_time: Utc::now() - Duration::minutes(30),
definition: StudySession {
id: "session".into(),
description: "session".into(),
parts: vec![
SessionPart::UnitFilter {
filter: UnitFilter::CourseFilter {
course_ids: vec!["0".into()],
},
duration: 15,
},
SessionPart::UnitFilter {
filter: UnitFilter::CourseFilter {
course_ids: vec!["1".into()],
},
duration: 30,
},
],
},
};
let mut simulation = TraneSimulation::new(500, Box::new(|_| Some(MasteryScore::Five)));
simulation.run_simulation(
&mut trane,
&vec![],
&Some(ExerciseFilter::StudySession(session_data)),
)?;
let matching_courses = [TestId(1, None, None)];
let exercise_ids = all_test_exercises(&LIBRARY);
for exercise_id in exercise_ids {
let exercise_ustr = exercise_id.to_ustr();
if matching_courses
.iter()
.any(|course| exercise_id.exercise_in_course(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)?;
} else {
assert!(
!simulation.answer_history.contains_key(&exercise_ustr),
"exercise {:?} should not have been scheduled",
exercise_id
);
}
}
Ok(())
}
#[test]
fn get_matching_courses() -> Result<()> {
let temp_dir = TempDir::new()?;
let trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let prefix = TestId(0, None, None).to_ustr();
let matching_courses = trane.get_matching_prefix(&prefix, Some(UnitType::Course));
assert_eq!(matching_courses.len(), 1);
assert!(matching_courses.contains(&prefix));
Ok(())
}
#[test]
fn get_matching_lessons() -> Result<()> {
let temp_dir = TempDir::new()?;
let trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let prefix = TestId(0, Some(0), None).to_ustr();
let matching_lessons = trane.get_matching_prefix(&prefix, Some(UnitType::Lesson));
assert_eq!(matching_lessons.len(), 1);
assert!(matching_lessons.contains(&prefix));
Ok(())
}
#[test]
fn get_matching_exercises() -> Result<()> {
let temp_dir = TempDir::new()?;
let trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let prefix = TestId(0, Some(0), Some(0)).to_ustr();
let matching_exercises = trane.get_matching_prefix(&prefix, Some(UnitType::Exercise));
assert_eq!(matching_exercises.len(), 1);
assert!(matching_exercises.contains(&prefix));
Ok(())
}
#[test]
fn get_matching_units() -> Result<()> {
let temp_dir = TempDir::new()?;
let trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let prefix = TestId(0, None, None).to_ustr();
let matching_units = trane.get_matching_prefix(&prefix, None);
assert_eq!(matching_units.len(), 23);
assert!(matching_units.contains(&prefix));
assert!(matching_units.contains(&TestId(0, Some(0), None).to_ustr()));
assert!(matching_units.contains(&TestId(0, Some(0), Some(0)).to_ustr()));
Ok(())
}
#[test]
fn set_scheduler_options() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let scheduler_options = SchedulerOptions {
batch_size: 10,
..Default::default()
};
trane.set_scheduler_options(scheduler_options);
let scheduler_options = trane.get_scheduler_options();
assert_eq!(scheduler_options.batch_size, 10);
Ok(())
}
#[test]
fn reset_scheduler_options() -> Result<()> {
let temp_dir = TempDir::new()?;
let mut trane = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let scheduler_options = SchedulerOptions {
batch_size: 10,
..Default::default()
};
trane.set_scheduler_options(scheduler_options);
trane.reset_scheduler_options();
let scheduler_options = trane.get_scheduler_options();
assert_eq!(
scheduler_options.batch_size,
SchedulerOptions::default().batch_size
);
Ok(())
}
#[test]
fn ignored_paths() -> Result<()> {
let user_preferences = UserPreferences {
ignored_paths: vec!["course_0/".to_owned(), "course_5/".to_owned()],
..Default::default()
};
let temp_dir = TempDir::new()?;
let course_builders = LIBRARY
.iter()
.map(|c| c.course_builder())
.collect::<Result<Vec<_>>>()?;
let trane = init_simulation(temp_dir.path(), &course_builders, Some(&user_preferences))?;
let exercise_ids = trane.get_all_exercise_ids(None);
assert!(!exercise_ids.is_empty());
assert!(exercise_ids.iter().all(|id| !id.starts_with("0::")));
assert!(exercise_ids.iter().all(|id| !id.starts_with("5::")));
Ok(())
}
#[test]
fn serialized_course_library() -> Result<()> {
let temp_dir = TempDir::new()?;
let _ = init_test_simulation(temp_dir.path(), &LIBRARY)?;
let course_library = LocalCourseLibrary::new(temp_dir.path(), UserPreferences::default())?;
let serialized_data = SerializedCourseLibrary::from(&course_library);
let encoded_data = postcard::to_stdvec(&serialized_data)?;
let decoded_data: SerializedCourseLibrary = postcard::from_bytes(&encoded_data)?;
assert_eq!(serialized_data, decoded_data);
let from_serialized_library = LocalCourseLibrary::new_from_serialized(
serialized_data.clone(),
UserPreferences::default(),
)?;
assert_eq!(
course_library.course_map,
from_serialized_library.course_map,
);
assert_eq!(
course_library.lesson_map,
from_serialized_library.lesson_map,
);
assert_eq!(
course_library.exercise_map,
from_serialized_library.exercise_map,
);
assert_eq!(
*course_library.unit_graph.read(),
*from_serialized_library.unit_graph.read(),
);
let trane = Trane::new_local_from_serialized(temp_dir.path(), serialized_data)?;
assert!(trane.get_all_exercise_ids(None).len() > 0);
Ok(())
}