use std::path::Path;
use anyhow::Result;
use rand::{Rng, RngExt};
use tempfile::TempDir;
use trane::{
Trane,
course_builder::{
AssetBuilder,
knowledge_base_builder::{CourseBuilder, ExerciseBuilder, LessonBuilder},
},
course_library::CourseLibrary,
data::{
CourseGenerator, CourseManifest, ExerciseType, MasteryScore,
course_generator::knowledge_base::{
KnowledgeBaseConfig, KnowledgeBaseExercise, KnowledgeBaseLesson,
},
},
test_utils::TraneSimulation,
};
use ustr::Ustr;
fn generate_lesson_dependencies(lesson_index: usize, rng: &mut impl Rng) -> Vec<Ustr> {
let num_dependencies = rng.random_range(0..=lesson_index);
if num_dependencies == 0 {
return vec![];
}
let mut dependencies = Vec::with_capacity(num_dependencies);
for _ in 0..num_dependencies.min(lesson_index) {
let dependency_id = Ustr::from(&format!("lesson_{}", rng.random_range(0..lesson_index)));
if dependencies.contains(&dependency_id) {
continue;
}
dependencies.push(dependency_id);
}
dependencies
}
fn knowledge_base_builder(
directory_name: &str,
course_manifest: CourseManifest,
num_lessons: usize,
num_exercises_per_lesson: usize,
) -> CourseBuilder {
let lessons = (0..num_lessons)
.map(|lesson_index| {
let lesson_id = Ustr::from(&format!("lesson_{}", lesson_index));
let exercises = (0..num_exercises_per_lesson)
.map(|exercise_index| {
let front_path = format!("exercise_{}.front.md", exercise_index);
let back_path = if exercise_index % 2 == 0 {
Some(format!("exercise_{}.back.md", exercise_index))
} else {
None
};
let mut asset_builders = vec![AssetBuilder {
file_name: front_path.clone(),
contents: "Front".into(),
}];
if let Some(back_path) = &back_path {
asset_builders.push(AssetBuilder {
file_name: back_path.clone(),
contents: "Back".into(),
});
}
ExerciseBuilder {
exercise: KnowledgeBaseExercise {
short_id: format!("exercise_{}", exercise_index),
short_lesson_id: lesson_id,
course_id: course_manifest.id,
front_file: front_path,
back_file: back_path,
name: None,
description: None,
exercise_type: None,
},
asset_builders,
}
})
.collect();
LessonBuilder {
lesson: KnowledgeBaseLesson {
short_id: lesson_id,
course_id: course_manifest.id,
dependencies: generate_lesson_dependencies(lesson_index, &mut rand::rng()),
encompassed: vec![],
superseded: vec![],
name: None,
description: None,
metadata: None,
has_instructions: false,
has_material: false,
default_exercise_type: Some(ExerciseType::Declarative),
},
exercises,
asset_builders: vec![],
}
})
.collect();
CourseBuilder {
directory_name: directory_name.into(),
lessons,
assets: vec![],
manifest: course_manifest,
}
}
fn init_knowledge_base_simulation(
library_root: &Path,
course_builders: &[CourseBuilder],
) -> Result<Trane> {
for builder in course_builders {
let course_root = library_root.join(&builder.directory_name);
builder.build(library_root)?;
std::fs::create_dir_all(course_root.join("not_a_lesson_directory"))?;
}
let trane = Trane::new_local(library_root, library_root)?;
Ok(trane)
}
#[test]
fn all_exercises_visited() -> Result<()> {
let course1_builder = knowledge_base_builder(
"course1",
CourseManifest {
id: Ustr::from("course1"),
name: "Course 1".into(),
description: None,
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
authors: None,
metadata: None,
course_material: None,
course_instructions: None,
generator_config: Some(CourseGenerator::KnowledgeBase(KnowledgeBaseConfig {
inlined: false,
})),
},
10,
5,
);
let course2_builder = knowledge_base_builder(
"course2",
CourseManifest {
id: Ustr::from("course2"),
name: "Course 2".into(),
description: None,
dependencies: vec!["course1".into()],
encompassed: vec![],
superseded: vec![],
authors: None,
metadata: None,
course_material: None,
course_instructions: None,
generator_config: Some(CourseGenerator::KnowledgeBase(KnowledgeBaseConfig {
inlined: true,
})),
},
10,
5,
);
let temp_dir = TempDir::new()?;
let mut trane =
init_knowledge_base_simulation(temp_dir.path(), &vec![course1_builder, course2_builder])?;
let exercise_ids = trane.get_all_exercise_ids(None);
assert!(!exercise_ids.is_empty());
let mut simulation = TraneSimulation::new(
exercise_ids.len() * 10,
Box::new(|_| Some(MasteryScore::Five)),
);
simulation.run_simulation(&mut trane, &vec![], &None)?;
let visited_exercises = simulation.answer_history.keys().collect::<Vec<_>>();
assert_eq!(visited_exercises.len(), exercise_ids.len());
Ok(())
}