use std::collections::HashMap;
use anyhow::Result;
use tempfile::TempDir;
use trane::{
Trane,
course_builder::{AssetBuilder, CourseBuilder},
course_library::CourseLibrary,
data::{
BasicAsset, CourseGenerator, CourseManifest, ExerciseAsset, ExerciseType,
LessonManifestBuilder, MasteryScore, UserPreferences,
course_generator::{
knowledge_base::KnowledgeBaseConfig,
literacy::LiteracyConfig,
transcription::{
Instrument, TranscriptionAsset, TranscriptionConfig, TranscriptionPassages,
TranscriptionPreferences,
},
},
},
test_utils::{TraneSimulation, assert_simulation_scores, init_simulation},
};
fn course_manifest(id: &str, generator: CourseGenerator) -> CourseManifest {
CourseManifest {
id: id.into(),
name: id.into(),
dependencies: vec![],
encompassed: vec![],
superseded: vec![],
description: None,
authors: None,
metadata: None,
course_material: None,
course_instructions: None,
generator_config: Some(generator),
}
}
fn course_builder(
id: &str,
generator: CourseGenerator,
asset_builders: Vec<AssetBuilder>,
) -> CourseBuilder {
CourseBuilder {
directory_name: id.into(),
course_manifest: course_manifest(id, generator),
lesson_manifest_template: LessonManifestBuilder::default(),
lesson_builders: vec![],
asset_builders,
}
}
fn assert_all_generated_exercises_visited(
trane: &mut Trane,
expected_exercises: usize,
) -> Result<()> {
let exercise_ids = trane.get_all_exercise_ids(None);
assert_eq!(exercise_ids.len(), expected_exercises);
let mut simulation = TraneSimulation::new(
exercise_ids.len() * 10,
Box::new(|_| Some(MasteryScore::Five)),
);
simulation.run_simulation(trane, &vec![], &None)?;
assert_eq!(simulation.answer_history.len(), exercise_ids.len());
for exercise_id in exercise_ids {
assert!(
simulation.answer_history.contains_key(&exercise_id),
"exercise {exercise_id} should have been scheduled"
);
assert_simulation_scores(exercise_id, trane, &simulation.answer_history)?;
}
Ok(())
}
#[test]
fn knowledge_base_course_generator_assets() -> Result<()> {
let generator = CourseGenerator::KnowledgeBase(KnowledgeBaseConfig { inlined: false });
let mut course_builder = course_builder(
"knowledge_base_course",
generator,
vec![
AssetBuilder {
file_name: "course.instructions.md".into(),
contents: "Course instructions".into(),
},
AssetBuilder {
file_name: "course.material.md".into(),
contents: "Course material".into(),
},
AssetBuilder {
file_name: "lesson_0.lesson/lesson.instructions.md".into(),
contents: "Lesson instructions".into(),
},
AssetBuilder {
file_name: "lesson_0.lesson/lesson.material.md".into(),
contents: "Lesson material".into(),
},
AssetBuilder {
file_name: "lesson_0.lesson/exercise_0.front.md".into(),
contents: "Front 0".into(),
},
AssetBuilder {
file_name: "lesson_0.lesson/exercise_0.back.md".into(),
contents: "Back 0".into(),
},
AssetBuilder {
file_name: "lesson_1.lesson/exercise_1.front.md".into(),
contents: "Front 1".into(),
},
AssetBuilder {
file_name: "lesson_1.lesson/exercise_1.back.md".into(),
contents: "Back 1".into(),
},
AssetBuilder {
file_name: "lesson_1.lesson/lesson.dependencies.json".into(),
contents: "[\"lesson_0\"]".into(),
},
],
);
course_builder.course_manifest.course_instructions = Some(BasicAsset::MarkdownAsset {
path: "course.instructions.md".into(),
});
course_builder.course_manifest.course_material = Some(BasicAsset::MarkdownAsset {
path: "course.material.md".into(),
});
let temp_dir = TempDir::new()?;
let mut trane = init_simulation(
temp_dir.path(),
&[course_builder],
Some(&UserPreferences::default()),
)?;
assert_all_generated_exercises_visited(&mut trane, 2)?;
let library_root = vfs::VfsPath::new(vfs::PhysicalFS::new(temp_dir.path()));
let exercise_ids = trane.get_all_exercise_ids(None);
assert_eq!(exercise_ids.len(), 2);
let course_manifest = trane
.get_course_manifest("knowledge_base_course".into())
.unwrap();
let assert_asset = |asset: &BasicAsset, expected_path: &str, expected_content: &str| {
assert!(matches!(asset, BasicAsset::MarkdownAsset { .. }));
if let BasicAsset::MarkdownAsset { path } = asset {
assert_eq!(path, expected_path);
assert_eq!(library_root.join(path)?.read_to_string()?, expected_content);
}
Ok::<(), anyhow::Error>(())
};
assert_asset(
course_manifest.course_instructions.as_ref().unwrap(),
"knowledge_base_course/course.instructions.md",
"Course instructions",
)?;
assert_asset(
course_manifest.course_material.as_ref().unwrap(),
"knowledge_base_course/course.material.md",
"Course material",
)?;
let lesson_manifest = trane
.get_lesson_manifest("knowledge_base_course::lesson_0".into())
.unwrap();
assert_asset(
lesson_manifest.lesson_instructions.as_ref().unwrap(),
"knowledge_base_course/lesson_0.lesson/lesson.instructions.md",
"Lesson instructions",
)?;
assert_asset(
lesson_manifest.lesson_material.as_ref().unwrap(),
"knowledge_base_course/lesson_0.lesson/lesson.material.md",
"Lesson material",
)?;
let expected_assets = [
(
"knowledge_base_course::lesson_0::exercise_0",
"Front 0",
"Back 0",
),
(
"knowledge_base_course::lesson_1::exercise_1",
"Front 1",
"Back 1",
),
];
for (exercise_id, expected_front, expected_back) in expected_assets {
let manifest = trane.get_exercise_manifest(exercise_id.into()).unwrap();
assert!(matches!(
&manifest.exercise_asset,
ExerciseAsset::FlashcardAsset { .. }
));
if let ExerciseAsset::FlashcardAsset {
front_path,
back_path: Some(back_path),
} = &manifest.exercise_asset
{
assert!(!front_path.starts_with('/'));
assert!(!back_path.starts_with('/'));
assert_eq!(
library_root.join(front_path)?.read_to_string()?,
expected_front
);
assert_eq!(
library_root.join(back_path)?.read_to_string()?,
expected_back
);
}
}
Ok(())
}
#[test]
fn literacy_course_generator() -> Result<()> {
let generator = CourseGenerator::Literacy(LiteracyConfig {
generate_dictation: true,
exercise_type: ExerciseType::Procedural,
});
let course_builder = course_builder(
"literacy_course",
generator,
vec![
AssetBuilder {
file_name: "lesson_0.lesson/example_0.example.md".into(),
contents: "Example 0".into(),
},
AssetBuilder {
file_name: "lesson_1.lesson/example_1.example.md".into(),
contents: "Example 1".into(),
},
AssetBuilder {
file_name: "lesson_1.lesson/lesson.dependencies.json".into(),
contents: "[\"lesson_0\"]".into(),
},
],
);
let temp_dir = TempDir::new()?;
let mut trane = init_simulation(
temp_dir.path(),
&[course_builder],
Some(&UserPreferences::default()),
)?;
assert_all_generated_exercises_visited(&mut trane, 4)
}
#[test]
fn transcription_course_generator() -> Result<()> {
let passage = TranscriptionPassages {
asset: TranscriptionAsset::Track {
short_id: "track".into(),
track_name: "Track".into(),
artist_name: None,
album_name: None,
duration: None,
external_link: None,
},
intervals: HashMap::from([(0, ("0:00".into(), "0:10".into()))]),
};
let generator = CourseGenerator::Transcription(TranscriptionConfig {
transcription_dependencies: vec![],
passage_directory: "passages".into(),
inlined_passages: vec![],
skip_singing_lessons: false,
skip_advanced_lessons: false,
});
let course_builder = course_builder(
"transcription_course",
generator,
vec![AssetBuilder {
file_name: "passages/track.json".into(),
contents: serde_json::to_string(&passage)?,
}],
);
let user_preferences = UserPreferences {
transcription: Some(TranscriptionPreferences {
instruments: vec![Instrument {
name: "Piano".into(),
id: "piano".into(),
}],
..Default::default()
}),
..Default::default()
};
let temp_dir = TempDir::new()?;
let mut trane = init_simulation(temp_dir.path(), &[course_builder], Some(&user_preferences))?;
assert_all_generated_exercises_visited(&mut trane, 4)
}