#![cfg_attr(coverage, feature(coverage_attribute))]
#![warn(clippy::pedantic)]
#![allow(clippy::doc_markdown)]
#![allow(clippy::float_cmp)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::module_name_repetitions)]
#![allow(clippy::wildcard_imports)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::too_many_lines)]
pub mod blacklist;
pub mod course_builder;
pub mod course_library;
pub mod data;
pub mod error;
pub mod exercise_scorer;
pub mod filter_manager;
pub mod graph;
pub mod practice_rewards;
pub mod practice_stats;
pub mod preferences_manager;
pub mod review_list;
pub mod reward_scorer;
pub mod scheduler;
pub mod study_session_manager;
#[cfg_attr(coverage, coverage(off))]
pub mod test_utils;
pub mod utils;
use anyhow::{Context, Result, bail, ensure};
use error::*;
use parking_lot::RwLock;
use std::{
fs::{File, create_dir},
io::Write,
path::Path,
sync::Arc,
};
use ustr::{Ustr, UstrMap, UstrSet};
use crate::{
blacklist::{Blacklist, LocalBlacklist},
course_library::{CourseLibrary, GetUnitGraph, LocalCourseLibrary, SerializedCourseLibrary},
data::{
CourseManifest, ExerciseManifest, ExerciseTrial, LessonManifest, MasteryScore,
SchedulerOptions, SchedulerPreferences, UnitReward, UnitType, UserPreferences,
filter::{ExerciseFilter, SavedFilter},
},
filter_manager::{FilterManager, LocalFilterManager},
graph::UnitGraph,
practice_rewards::{LocalPracticeRewards, PracticeRewards},
practice_stats::{LocalPracticeStats, PracticeStats},
preferences_manager::{LocalPreferencesManager, PreferencesManager},
review_list::{LocalReviewList, ReviewList},
scheduler::{DepthFirstScheduler, ExerciseScheduler, data::SchedulerData},
study_session_manager::{LocalStudySessionManager, StudySessionManager},
};
pub const TRANE_CONFIG_DIR_PATH: &str = ".trane";
pub const PRACTICE_STATS_PATH: &str = "practice_stats.db";
pub const PRACTICE_REWARDS_PATH: &str = "practice_rewards.db";
pub const BLACKLIST_PATH: &str = "blacklist.db";
pub const REVIEW_LIST_PATH: &str = "review_list.db";
pub const FILTERS_DIR: &str = "filters";
pub const STUDY_SESSIONS_DIR: &str = "study_sessions";
pub const USER_PREFERENCES_PATH: &str = "user_preferences.json";
pub struct Trane {
library_root: String,
blacklist: Arc<RwLock<dyn Blacklist + Send + Sync>>,
course_library: Arc<RwLock<dyn CourseLibrary + Send + Sync>>,
filter_manager: Arc<RwLock<dyn FilterManager + Send + Sync>>,
practice_stats: Arc<RwLock<dyn PracticeStats + Send + Sync>>,
practice_rewards: Arc<RwLock<dyn PracticeRewards + Send + Sync>>,
preferences_manager: Arc<RwLock<dyn PreferencesManager + Send + Sync>>,
review_list: Arc<RwLock<dyn ReviewList + Send + Sync>>,
scheduler_data: SchedulerData,
scheduler: DepthFirstScheduler,
study_session_manager: Arc<RwLock<dyn StudySessionManager + Send + Sync>>,
unit_graph: Arc<RwLock<dyn UnitGraph + Send + Sync>>,
}
impl Trane {
#[cfg_attr(coverage, coverage(off))]
fn create_scheduler_options(preferences: Option<&SchedulerPreferences>) -> SchedulerOptions {
let mut options = SchedulerOptions::default();
if let Some(preferences) = preferences
&& let Some(batch_size) = preferences.batch_size
{
options.batch_size = batch_size;
}
options
}
#[cfg_attr(coverage, coverage(off))]
fn init_config_directory(library_root: &Path) -> Result<()> {
ensure!(
library_root.is_dir(),
"library root {} is not a directory",
library_root.display(),
);
let trane_path = library_root.join(TRANE_CONFIG_DIR_PATH);
if !trane_path.exists() {
create_dir(trane_path.clone()).context("failed to create config directory")?;
} else if !trane_path.is_dir() {
bail!("config path .trane inside library must be a directory");
}
let filters_path = trane_path.join(FILTERS_DIR);
if !filters_path.is_dir() {
create_dir(filters_path.clone()).context("failed to create filters directory")?;
}
let sessions_path = trane_path.join(STUDY_SESSIONS_DIR);
if !sessions_path.is_dir() {
create_dir(sessions_path.clone())
.context("failed to create study_sessions directory")?;
}
let user_prefs_path = trane_path.join(USER_PREFERENCES_PATH);
if !user_prefs_path.exists() {
let mut file = File::create(user_prefs_path.clone())
.context("failed to create user_preferences.json file")?;
let default_prefs = UserPreferences::default();
let prefs_json = serde_json::to_string_pretty(&default_prefs)? + "\n";
file.write_all(prefs_json.as_bytes())
.context("failed to write to user_preferences.json file")?;
} else if !user_prefs_path.is_file() {
bail!("user preferences file must be a regular file");
}
Ok(())
}
#[cfg_attr(coverage, coverage(off))]
fn new_local_helper(
library_root: &Path,
preferences_manager: Arc<RwLock<LocalPreferencesManager>>,
course_library: Arc<RwLock<LocalCourseLibrary>>,
) -> Result<Trane> {
let config_path = library_root.join(Path::new(TRANE_CONFIG_DIR_PATH));
let user_preferences = preferences_manager.read().get_user_preferences()?;
let unit_graph = course_library.write().get_unit_graph();
let practice_stats = Arc::new(RwLock::new(LocalPracticeStats::new_from_disk(
config_path.join(PRACTICE_STATS_PATH).to_str().unwrap(),
)?));
let practice_rewards = Arc::new(RwLock::new(LocalPracticeRewards::new_from_disk(
config_path.join(PRACTICE_REWARDS_PATH).to_str().unwrap(),
)?));
let blacklist = Arc::new(RwLock::new(LocalBlacklist::new_from_disk(
config_path.join(BLACKLIST_PATH).to_str().unwrap(),
)?));
let review_list = Arc::new(RwLock::new(LocalReviewList::new_from_disk(
config_path.join(REVIEW_LIST_PATH).to_str().unwrap(),
)?));
let filter_manager = Arc::new(RwLock::new(LocalFilterManager::new(
config_path.join(FILTERS_DIR).to_str().unwrap(),
)?));
let study_sessions_manager = Arc::new(RwLock::new(LocalStudySessionManager::new(
config_path.join(STUDY_SESSIONS_DIR).to_str().unwrap(),
)?));
let options = Self::create_scheduler_options(user_preferences.scheduler.as_ref());
options.verify()?;
let scheduler_data = SchedulerData {
options,
course_library: course_library.clone(),
unit_graph: unit_graph.clone(),
practice_stats: practice_stats.clone(),
practice_rewards: practice_rewards.clone(),
blacklist: blacklist.clone(),
review_list: review_list.clone(),
filter_manager: filter_manager.clone(),
frequency_map: Arc::new(RwLock::new(UstrMap::default())),
trial_counts: Arc::new(RwLock::new((0, 0))),
};
Ok(Trane {
blacklist,
course_library,
filter_manager,
library_root: library_root.to_str().unwrap().to_string(),
practice_stats,
practice_rewards,
preferences_manager,
review_list,
scheduler_data: scheduler_data.clone(),
scheduler: DepthFirstScheduler::new(scheduler_data),
study_session_manager: study_sessions_manager,
unit_graph,
})
}
#[cfg_attr(coverage, coverage(off))]
pub fn new_local(working_dir: &Path, library_root: &Path) -> Result<Trane> {
Self::init_config_directory(library_root)?;
let preferences_manager = Arc::new(RwLock::new(LocalPreferencesManager {
path: library_root
.join(TRANE_CONFIG_DIR_PATH)
.join(USER_PREFERENCES_PATH),
}));
let user_preferences = preferences_manager.read().get_user_preferences()?;
let course_library = Arc::new(RwLock::new(LocalCourseLibrary::new(
&working_dir.join(library_root),
user_preferences.clone(),
)?));
Self::new_local_helper(library_root, preferences_manager, course_library)
}
#[cfg_attr(coverage, coverage(off))]
pub fn new_local_from_serialized(
library_root: &Path,
serialized_library: SerializedCourseLibrary,
) -> Result<Trane> {
Self::init_config_directory(library_root)?;
let preferences_manager = Arc::new(RwLock::new(LocalPreferencesManager {
path: library_root
.join(TRANE_CONFIG_DIR_PATH)
.join(USER_PREFERENCES_PATH),
}));
let user_preferences = preferences_manager.read().get_user_preferences()?;
let course_library = Arc::new(RwLock::new(LocalCourseLibrary::new_from_serialized(
serialized_library,
user_preferences.clone(),
)?));
Self::new_local_helper(library_root, preferences_manager, course_library)
}
pub fn library_root(&self) -> String {
self.library_root.clone()
}
#[allow(dead_code)]
fn get_scheduler_data(&self) -> SchedulerData {
self.scheduler_data.clone()
}
}
#[cfg_attr(coverage, coverage(off))]
impl Blacklist for Trane {
fn add_to_blacklist(&mut self, unit_id: Ustr) -> Result<(), BlacklistError> {
self.scheduler.invalidate_cached_score(unit_id);
self.blacklist.write().add_to_blacklist(unit_id)
}
fn remove_from_blacklist(&mut self, unit_id: Ustr) -> Result<(), BlacklistError> {
self.scheduler.invalidate_cached_score(unit_id);
self.blacklist.write().remove_from_blacklist(unit_id)
}
fn remove_prefix_from_blacklist(&mut self, prefix: &str) -> Result<(), BlacklistError> {
self.scheduler.invalidate_cached_scores_with_prefix(prefix);
self.blacklist.write().remove_prefix_from_blacklist(prefix)
}
fn blacklisted(&self, unit_id: Ustr) -> Result<bool, BlacklistError> {
self.blacklist.read().blacklisted(unit_id)
}
fn get_blacklist_entries(&self) -> Result<Vec<Ustr>, BlacklistError> {
self.blacklist.read().get_blacklist_entries()
}
}
#[cfg_attr(coverage, coverage(off))]
impl CourseLibrary for Trane {
fn get_course_manifest(&self, course_id: Ustr) -> Option<Arc<CourseManifest>> {
self.course_library.read().get_course_manifest(course_id)
}
fn get_lesson_manifest(&self, lesson_id: Ustr) -> Option<Arc<LessonManifest>> {
self.course_library.read().get_lesson_manifest(lesson_id)
}
fn get_exercise_manifest(&self, exercise_id: Ustr) -> Option<Arc<ExerciseManifest>> {
self.course_library
.read()
.get_exercise_manifest(exercise_id)
}
fn get_course_ids(&self) -> Vec<Ustr> {
self.course_library.read().get_course_ids()
}
fn get_lesson_ids(&self, course_id: Ustr) -> Option<Vec<Ustr>> {
self.course_library.read().get_lesson_ids(course_id)
}
fn get_exercise_ids(&self, lesson_id: Ustr) -> Option<Vec<Ustr>> {
self.course_library.read().get_exercise_ids(lesson_id)
}
fn get_all_exercise_ids(&self, unit_id: Option<Ustr>) -> Vec<Ustr> {
self.course_library.read().get_all_exercise_ids(unit_id)
}
fn get_matching_prefix(&self, prefix: &str, unit_type: Option<UnitType>) -> UstrSet {
self.course_library
.read()
.get_matching_prefix(prefix, unit_type)
}
}
#[cfg_attr(coverage, coverage(off))]
impl ExerciseScheduler for Trane {
fn get_exercise_batch(
&self,
filter: Option<ExerciseFilter>,
) -> Result<Vec<ExerciseManifest>, ExerciseSchedulerError> {
self.scheduler.get_exercise_batch(filter)
}
fn score_exercise(
&self,
exercise_id: Ustr,
score: MasteryScore,
timestamp: i64,
) -> Result<(), ExerciseSchedulerError> {
self.scheduler.score_exercise(exercise_id, score, timestamp)
}
fn get_unit_score(&self, unit_id: Ustr) -> Result<Option<f32>, ExerciseSchedulerError> {
self.scheduler.get_unit_score(unit_id)
}
fn invalidate_cached_score(&self, unit_id: Ustr) {
self.scheduler.invalidate_cached_score(unit_id);
}
fn invalidate_cached_scores_with_prefix(&self, prefix: &str) {
self.scheduler.invalidate_cached_scores_with_prefix(prefix);
}
fn get_scheduler_options(&self) -> SchedulerOptions {
self.scheduler.get_scheduler_options()
}
fn set_scheduler_options(&mut self, options: SchedulerOptions) {
self.scheduler.set_scheduler_options(options);
}
fn reset_scheduler_options(&mut self) {
self.scheduler.reset_scheduler_options();
}
}
#[cfg_attr(coverage, coverage(off))]
impl FilterManager for Trane {
fn get_filter(&self, id: &str) -> Option<Arc<SavedFilter>> {
self.filter_manager.read().get_filter(id)
}
fn list_filters(&self) -> Vec<(String, String)> {
self.filter_manager.read().list_filters()
}
}
#[cfg_attr(coverage, coverage(off))]
impl PracticeRewards for Trane {
fn get_rewards(
&self,
unit_id: Ustr,
num_rewards: u32,
) -> Result<Vec<data::UnitReward>, PracticeRewardsError> {
self.practice_rewards
.read()
.get_rewards(unit_id, num_rewards)
}
fn record_unit_rewards(
&mut self,
rewards: &[UnitReward],
) -> Result<Vec<Ustr>, PracticeRewardsError> {
self.practice_rewards.write().record_unit_rewards(rewards)
}
fn trim_rewards(&mut self, num_rewards: u32) -> Result<(), PracticeRewardsError> {
self.practice_rewards.write().trim_rewards(num_rewards)
}
fn remove_rewards_with_prefix(&mut self, prefix: &str) -> Result<(), PracticeRewardsError> {
self.practice_rewards
.write()
.remove_rewards_with_prefix(prefix)
}
}
#[cfg_attr(coverage, coverage(off))]
impl PracticeStats for Trane {
fn get_scores(
&self,
exercise_id: Ustr,
num_scores: u32,
) -> Result<Vec<ExerciseTrial>, PracticeStatsError> {
self.practice_stats
.read()
.get_scores(exercise_id, num_scores)
}
fn record_exercise_score(
&mut self,
exercise_id: Ustr,
score: MasteryScore,
timestamp: i64,
) -> Result<(), PracticeStatsError> {
self.practice_stats
.write()
.record_exercise_score(exercise_id, score, timestamp)
}
fn trim_scores(&mut self, num_scores: u32) -> Result<(), PracticeStatsError> {
self.practice_stats.write().trim_scores(num_scores)
}
fn remove_scores_with_prefix(&mut self, prefix: &str) -> Result<(), PracticeStatsError> {
self.practice_stats
.write()
.remove_scores_with_prefix(prefix)
}
}
#[cfg_attr(coverage, coverage(off))]
impl PreferencesManager for Trane {
fn get_user_preferences(&self) -> Result<UserPreferences, PreferencesManagerError> {
self.preferences_manager.read().get_user_preferences()
}
fn set_user_preferences(
&mut self,
preferences: UserPreferences,
) -> Result<(), PreferencesManagerError> {
self.preferences_manager
.write()
.set_user_preferences(preferences)
}
}
#[cfg_attr(coverage, coverage(off))]
impl ReviewList for Trane {
fn add_to_review_list(&mut self, unit_id: Ustr) -> Result<(), ReviewListError> {
self.review_list.write().add_to_review_list(unit_id)
}
fn remove_from_review_list(&mut self, unit_id: Ustr) -> Result<(), ReviewListError> {
self.review_list.write().remove_from_review_list(unit_id)
}
fn get_review_list_entries(&self) -> Result<Vec<Ustr>, ReviewListError> {
self.review_list.read().get_review_list_entries()
}
}
#[cfg_attr(coverage, coverage(off))]
impl StudySessionManager for Trane {
fn get_study_session(&self, id: &str) -> Option<data::filter::StudySession> {
self.study_session_manager.read().get_study_session(id)
}
fn list_study_sessions(&self) -> Vec<(String, String)> {
self.study_session_manager.read().list_study_sessions()
}
}
#[cfg_attr(coverage, coverage(off))]
impl UnitGraph for Trane {
fn add_course(&mut self, course_id: Ustr) -> Result<(), UnitGraphError> {
self.unit_graph.write().add_course(course_id)
}
fn add_lesson(&mut self, lesson_id: Ustr, course_id: Ustr) -> Result<(), UnitGraphError> {
self.unit_graph.write().add_lesson(lesson_id, course_id)
}
fn add_exercise(&mut self, exercise_id: Ustr, lesson_id: Ustr) -> Result<(), UnitGraphError> {
self.unit_graph.write().add_exercise(exercise_id, lesson_id)
}
fn add_dependencies(
&mut self,
unit_id: Ustr,
unit_type: UnitType,
dependencies: &[Ustr],
) -> Result<(), UnitGraphError> {
self.unit_graph
.write()
.add_dependencies(unit_id, unit_type, dependencies)
}
fn add_encompassed(
&mut self,
unit_id: Ustr,
dependencies: &[Ustr],
encompassed: &[(Ustr, f32)],
) -> Result<(), UnitGraphError> {
self.unit_graph
.write()
.add_encompassed(unit_id, dependencies, encompassed)
}
fn set_encompasing_equals_dependency(&mut self) {
self.unit_graph.write().encompasing_equals_dependency();
}
fn encompasing_equals_dependency(&self) -> bool {
self.unit_graph.read().encompasing_equals_dependency()
}
fn add_superseded(&mut self, unit_id: Ustr, superseded: &[Ustr]) {
self.unit_graph.write().add_superseded(unit_id, superseded);
}
fn get_unit_type(&self, unit_id: Ustr) -> Option<UnitType> {
self.unit_graph.read().get_unit_type(unit_id)
}
fn get_course_lessons(&self, course_id: Ustr) -> Option<Arc<UstrSet>> {
self.unit_graph.read().get_course_lessons(course_id)
}
fn get_starting_lessons(&self, course_id: Ustr) -> Option<Arc<UstrSet>> {
self.unit_graph.read().get_starting_lessons(course_id)
}
fn update_starting_lessons(&mut self) {
self.unit_graph.write().update_starting_lessons();
}
fn get_lesson_course(&self, lesson_id: Ustr) -> Option<Ustr> {
self.unit_graph.read().get_lesson_course(lesson_id)
}
fn get_lesson_exercises(&self, lesson_id: Ustr) -> Option<Arc<UstrSet>> {
self.unit_graph.read().get_lesson_exercises(lesson_id)
}
fn get_exercise_lesson(&self, exercise_id: Ustr) -> Option<Ustr> {
self.unit_graph.read().get_exercise_lesson(exercise_id)
}
fn get_dependencies(&self, unit_id: Ustr) -> Option<Arc<UstrSet>> {
self.unit_graph.read().get_dependencies(unit_id)
}
fn get_dependents(&self, unit_id: Ustr) -> Option<Arc<UstrSet>> {
self.unit_graph.read().get_dependents(unit_id)
}
fn get_encompasses(&self, unit_id: Ustr) -> Option<Vec<(Ustr, f32)>> {
self.unit_graph.read().get_encompasses(unit_id)
}
fn get_encompassed_by(&self, unit_id: Ustr) -> Option<Vec<(Ustr, f32)>> {
self.unit_graph.read().get_encompassed_by(unit_id)
}
fn get_dependency_sinks(&self) -> Arc<UstrSet> {
self.unit_graph.read().get_dependency_sinks()
}
fn get_supersedes(&self, unit_id: Ustr) -> Option<Arc<UstrSet>> {
self.unit_graph.read().get_supersedes(unit_id)
}
fn get_superseded_by(&self, unit_id: Ustr) -> Option<Arc<UstrSet>> {
self.unit_graph.read().get_superseded_by(unit_id)
}
fn check_cycles(&self) -> Result<(), UnitGraphError> {
self.unit_graph.read().check_cycles()
}
fn generate_dot_graph(&self, courses_only: bool) -> String {
self.unit_graph.read().generate_dot_graph(courses_only)
}
}
unsafe impl Send for Trane {}
unsafe impl Sync for Trane {}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod test {
use anyhow::Result;
use std::{fs::*, os::unix::prelude::PermissionsExt};
use crate::{
FILTERS_DIR, STUDY_SESSIONS_DIR, TRANE_CONFIG_DIR_PATH, Trane, USER_PREFERENCES_PATH,
data::{SchedulerOptions, SchedulerPreferences, UserPreferences},
};
#[test]
fn library_root() -> Result<()> {
let dir = tempfile::tempdir()?;
let trane = Trane::new_local(dir.path(), dir.path())?;
assert_eq!(trane.library_root(), dir.path().to_str().unwrap());
Ok(())
}
#[test]
fn library_root_is_not_dir() -> Result<()> {
let file = tempfile::NamedTempFile::new()?;
let result = Trane::new_local(file.path(), file.path());
assert!(result.is_err());
Ok(())
}
#[test]
fn config_dir_is_file() -> Result<()> {
let dir = tempfile::tempdir()?;
let trane_path = dir.path().join(".trane");
File::create(trane_path)?;
assert!(Trane::new_local(dir.path(), dir.path()).is_err());
Ok(())
}
#[test]
fn bad_dir_permissions() -> Result<()> {
let dir = tempfile::tempdir()?;
set_permissions(&dir, Permissions::from_mode(0o000))?;
assert!(Trane::new_local(dir.path(), dir.path()).is_err());
Ok(())
}
#[test]
fn bad_config_dir_permissions() -> Result<()> {
let dir = tempfile::tempdir()?;
let config_dir_path = dir.path().join(".trane");
create_dir(&config_dir_path)?;
set_permissions(&config_dir_path, Permissions::from_mode(0o000))?;
assert!(Trane::new_local(dir.path(), dir.path()).is_err());
Ok(())
}
#[test]
fn user_preferences_file_is_a_dir() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
std::fs::create_dir_all(
temp_dir
.path()
.join(TRANE_CONFIG_DIR_PATH)
.join(USER_PREFERENCES_PATH),
)?;
assert!(Trane::new_local(temp_dir.path(), temp_dir.path()).is_err());
Ok(())
}
#[test]
fn cannot_create_filters_directory() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let config_dir = temp_dir.path().join(TRANE_CONFIG_DIR_PATH);
create_dir(config_dir.clone())?;
std::fs::set_permissions(temp_dir.path(), std::fs::Permissions::from_mode(0o444))?;
assert!(Trane::new_local(temp_dir.path(), temp_dir.path()).is_err());
Ok(())
}
#[test]
fn cannot_create_study_sessions() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let config_dir = temp_dir.path().join(TRANE_CONFIG_DIR_PATH);
create_dir(config_dir.clone())?;
let filters_dir = config_dir.join(FILTERS_DIR);
create_dir(filters_dir)?;
std::fs::set_permissions(config_dir, std::fs::Permissions::from_mode(0o500))?;
assert!(Trane::new_local(temp_dir.path(), temp_dir.path()).is_err());
Ok(())
}
#[test]
fn cannot_create_user_preferences() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let config_dir = temp_dir.path().join(TRANE_CONFIG_DIR_PATH);
create_dir(config_dir.clone())?;
let filters_dir = config_dir.join(FILTERS_DIR);
create_dir(filters_dir)?;
let sessions_dir = config_dir.join(STUDY_SESSIONS_DIR);
create_dir(sessions_dir)?;
std::fs::set_permissions(config_dir, std::fs::Permissions::from_mode(0o500))?;
assert!(Trane::new_local(temp_dir.path(), temp_dir.path()).is_err());
Ok(())
}
#[test]
fn scheduler_data() -> Result<()> {
let dir = tempfile::tempdir()?;
let trane = Trane::new_local(dir.path(), dir.path())?;
trane.get_scheduler_data();
Ok(())
}
#[test]
fn scheduler_options() {
let user_preferences = UserPreferences {
scheduler: None,
transcription: None,
ignored_paths: vec![],
};
let options = Trane::create_scheduler_options(user_preferences.scheduler.as_ref());
assert_eq!(options.batch_size, SchedulerOptions::default().batch_size);
let user_preferences = UserPreferences {
scheduler: Some(SchedulerPreferences {
batch_size: Some(10),
}),
transcription: None,
ignored_paths: vec![],
};
let options = Trane::create_scheduler_options(user_preferences.scheduler.as_ref());
assert_eq!(options.batch_size, 10);
}
}