• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

trane-project / trane / 12735888711

12 Jan 2025 06:06PM UTC coverage: 92.973% (-6.9%) from 99.866%
12735888711

Pull #319

github

web-flow
Merge 2bba403ef into f4478d7eb
Pull Request #319: Update to rust 1.84

69 of 77 new or added lines in 18 files covered. (89.61%)

236 existing lines in 13 files now uncovered.

9672 of 10403 relevant lines covered (92.97%)

87046.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.52
/src/lib.rs
1
//! Trane is an automated practice system for the acquisition of complex and highly hierarchical
2
//! skills. It is based on the principles of spaced repetition, mastery learning, and chunking.
3
//!
4
//! Given a set of exercises which have been bundled into lessons and further bundled in courses, as
5
//! well as the dependency relationships between those lessons and courses, Trane selects exercises
6
//! to present to the user. It makes sure that exercises from a course or lesson are not presented
7
//! to the user until the exercises in their dependencies have been sufficiently mastered. It also
8
//! makes sure to keep the balance of exercises so that the difficulty of the exercises lies
9
//! slightly outside the user's current mastery.
10
//!
11
//! You can think of this process as progressing through the skill tree of a character in a video
12
//! game, but applied to arbitrary skills, which are defined in plain-text files which define the
13
//! exercises, their bundling into lessons and courses, and the dependency relationships between
14
//! them.
15
//!
16
//! Trane is named after John Coltrane, whose nickname Trane was often used in wordplay with the
17
//! word train (as in the vehicle) to describe the overwhelming power of his playing. It is used
18
//! here as a play on its homophone (as in "training a new skill").
19
//!
20
//@<lp-example-3
21
//! Below is an overview of some of the most important modules in this crate and their purpose.
22
//! Refer to the documentation of each module for more details.
23
//!
24
//! - [`data`]: Contains the basic data structures used by Trane.
25
//! - [`graph`]: Defines the graph used by Trane to list the units of material and the dependencies
26
//!   among them.
27
//! - [`course_library`]: Reads a collection of courses, lessons, and exercises from the file system
28
//!   and provides basic utilities for working with them.
29
//! - [`scheduler`]: Defines the algorithm used by Trane to select exercises to present to the user.
30
//! - [`practice_stats`]: Stores the results of practice sessions for use in determining the next
31
//!   batch of exercises.
32
//! - [`blacklist`]: Defines the list of units the student wishes to hide, either because their
33
//!   material has already been mastered or they do not wish to learn it.
34
//! - [`scorer`]: Calculates a score for an exercise based on the results and timestamps of previous
35
//!   trials.
36
//!
37
//>@lp-example-3
38

39
// Use pedantic warnings but disable some that are not useful.
40
#![warn(clippy::pedantic)]
41
#![allow(clippy::doc_markdown)]
42
#![allow(clippy::float_cmp)]
43
#![allow(clippy::missing_errors_doc)]
44
#![allow(clippy::missing_panics_doc)]
45
#![allow(clippy::module_name_repetitions)]
46
#![allow(clippy::wildcard_imports)]
47
#![allow(clippy::cast_possible_truncation)]
48
#![allow(clippy::cast_sign_loss)]
49
#![allow(clippy::cast_precision_loss)]
50
#![allow(clippy::too_many_lines)]
51

52
pub mod blacklist;
53
pub mod course_builder;
54
pub mod course_library;
55
pub mod data;
56
pub mod db_utils;
57
pub mod error;
58
pub mod filter_manager;
59
pub mod graph;
60
pub mod mantra_miner;
61
pub mod practice_stats;
62
pub mod preferences_manager;
63
pub mod repository_manager;
64
pub mod review_list;
65
pub mod scheduler;
66
pub mod scorer;
67
pub mod study_session_manager;
68
pub mod testutil;
69
pub mod transcription_downloader;
70

71
use anyhow::{bail, ensure, Context, Result};
72
use error::*;
73
use parking_lot::RwLock;
74
use preferences_manager::{LocalPreferencesManager, PreferencesManager};
75
use review_list::{LocalReviewList, ReviewList};
76
use std::{
77
    fs::{create_dir, File},
78
    io::Write,
79
    path::Path,
80
    sync::Arc,
81
};
82
use study_session_manager::{LocalStudySessionManager, StudySessionManager};
83
use transcription_downloader::{LocalTranscriptionDownloader, TranscriptionDownloader};
84
use ustr::{Ustr, UstrMap, UstrSet};
85

86
use crate::{
87
    blacklist::{Blacklist, LocalBlacklist},
88
    course_library::{CourseLibrary, GetUnitGraph, LocalCourseLibrary},
89
    data::{
90
        filter::{ExerciseFilter, SavedFilter},
91
        CourseManifest, ExerciseManifest, ExerciseTrial, LessonManifest, MasteryScore,
92
        RepositoryMetadata, SchedulerOptions, SchedulerPreferences, UnitType, UserPreferences,
93
    },
94
    filter_manager::{FilterManager, LocalFilterManager},
95
    graph::UnitGraph,
96
    mantra_miner::TraneMantraMiner,
97
    practice_stats::{LocalPracticeStats, PracticeStats},
98
    repository_manager::{LocalRepositoryManager, RepositoryManager},
99
    scheduler::{data::SchedulerData, DepthFirstScheduler, ExerciseScheduler},
100
};
101

102
/// The path to the folder inside each course library containing the user data.
103
pub const TRANE_CONFIG_DIR_PATH: &str = ".trane";
104

105
/// The path to the `SQLite` database containing the results of previous exercise trials.
106
pub const PRACTICE_STATS_PATH: &str = "practice_stats.db";
107

108
/// The path to the `SQLite` database containing the list of units to ignore during scheduling.
109
pub const BLACKLIST_PATH: &str = "blacklist.db";
110

111
/// The path to the `SQLite` database containing the list of units the student wishes to review.
112
pub const REVIEW_LIST_PATH: &str = "review_list.db";
113

114
/// The path to the directory containing unit filters saved by the user.
115
pub const FILTERS_DIR: &str = "filters";
116

117
/// The path to the directory containing study sessions saved by the user.
118
pub const STUDY_SESSIONS_DIR: &str = "study_sessions";
119

120
/// The path to the file containing user preferences.
121
pub const USER_PREFERENCES_PATH: &str = "user_preferences.json";
122

123
/// The name of the directory where repositories will be downloaded.
124
const DOWNLOAD_DIRECTORY: &str = "managed_courses";
125

126
/// The name of the directory where the details on all repositories will be stored. This directory
127
/// will be created under the `.trane` directory at the root of the Trane library.
128
const REPOSITORY_DIRECTORY: &str = "repositories";
129

130
/// Trane is a library for the acquisition of highly hierarchical knowledge and skills based on the
131
/// principles of mastery learning and spaced repetition. Given a list of courses, its lessons and
132
/// corresponding exercises, Trane presents the student with a list of exercises based on the
133
/// demonstrated mastery of previous exercises. It makes sure that new material and skills are not
134
/// introduced until the prerequisite material and skills have been sufficiently mastered.
135
pub struct Trane {
136
    /// The path to the root of the course library.
137
    library_root: String,
138

139
    /// The object managing the list of courses, lessons, and exercises to be skipped.
140
    blacklist: Arc<RwLock<dyn Blacklist + Send + Sync>>,
141

142
    /// The object managing all the course, lesson, and exercise info.
143
    course_library: Arc<RwLock<dyn CourseLibrary + Send + Sync>>,
144

145
    /// The object managing unit filters saved by the user.
146
    filter_manager: Arc<RwLock<dyn FilterManager + Send + Sync>>,
147

148
    /// The object managing the information on previous exercise trials.
149
    practice_stats: Arc<RwLock<dyn PracticeStats + Send + Sync>>,
150

151
    /// The object managing the user preferences.
152
    preferences_manager: Arc<RwLock<dyn PreferencesManager + Send + Sync>>,
153

154
    /// The object managing git repositories containing courses.
155
    repo_manager: Arc<RwLock<dyn RepositoryManager + Send + Sync>>,
156

157
    /// The object managing the list of units to review.
158
    review_list: Arc<RwLock<dyn ReviewList + Send + Sync>>,
159

160
    /// The object managing access to all the data needed by the scheduler. It's saved separately
161
    /// from the scheduler so that tests can have access to it.
162
    scheduler_data: SchedulerData,
163

164
    /// The object managing the scheduling algorithm.
165
    scheduler: DepthFirstScheduler,
166

167
    /// The object managing the study sessions saved by the user.
168
    study_session_manager: Arc<RwLock<dyn StudySessionManager + Send + Sync>>,
169

170
    /// The dependency graph of courses and lessons in the course library.
171
    unit_graph: Arc<RwLock<dyn UnitGraph + Send + Sync>>,
172

173
    /// An optional instance of the transcription downloader.
174
    transcription_downloader: Arc<RwLock<dyn TranscriptionDownloader + Send + Sync>>,
175

176
    /// An instance of the mantra miner that "recites" Tara Sarasvati's mantra while Trane runs.
177
    mantra_miner: TraneMantraMiner,
178
}
179

180
impl Trane {
181
    /// Creates the scheduler options, overriding any values with those specified in the user
182
    /// preferences.
183
    fn create_scheduler_options(preferences: Option<&SchedulerPreferences>) -> SchedulerOptions {
73✔
184
        let mut options = SchedulerOptions::default();
73✔
185
        if let Some(preferences) = preferences {
73✔
186
            if let Some(batch_size) = preferences.batch_size {
1✔
187
                options.batch_size = batch_size;
1✔
188
            }
1✔
189
        }
72✔
190
        options
73✔
191
    }
73✔
192

193
    /// Initializes the config directory at path `.trane` inside the library root.
194
    fn init_config_directory(library_root: &Path) -> Result<()> {
79✔
195
        // Verify that the library root is a directory.
79✔
196
        ensure!(
79✔
197
            library_root.is_dir(),
79✔
198
            "library root {} is not a directory",
1✔
199
            library_root.display(),
1✔
200
        );
201

202
        // Create the config folder inside the library root if it does not exist already.
203
        let trane_path = library_root.join(TRANE_CONFIG_DIR_PATH);
78✔
204
        if !trane_path.exists() {
78✔
205
            create_dir(trane_path.clone()).context("failed to create config directory")?;
66✔
206
        } else if !trane_path.is_dir() {
12✔
207
            bail!("config path .trane inside library must be a directory");
1✔
208
        }
11✔
209

210
        // Create the `filters` directory if it does not exist already.
211
        let filters_path = trane_path.join(FILTERS_DIR);
75✔
212
        if !filters_path.is_dir() {
75✔
213
            create_dir(filters_path.clone()).context("failed to create filters directory")?;
73✔
214
        }
2✔
215

216
        // Create the `study_sessions` directory if it does not exist already.
217
        let sessions_path = trane_path.join(STUDY_SESSIONS_DIR);
74✔
218
        if !sessions_path.is_dir() {
74✔
219
            create_dir(sessions_path.clone())
73✔
220
                .context("failed to create study_sessions directory")?;
73✔
221
        }
1✔
222

223
        // Create the user preferences file if it does not exist already.
224
        let user_prefs_path = trane_path.join(USER_PREFERENCES_PATH);
73✔
225
        if !user_prefs_path.exists() {
73✔
226
            let mut file = File::create(user_prefs_path.clone())
65✔
227
                .context("failed to create user_preferences.json file")?;
65✔
228
            let default_prefs = UserPreferences::default();
64✔
229
            let prefs_json = serde_json::to_string_pretty(&default_prefs)? + "\n";
64✔
230
            file.write_all(prefs_json.as_bytes())
64✔
231
                .context("failed to write to user_preferences.json file")?;
64✔
232
        } else if !user_prefs_path.is_file() {
8✔
233
            bail!("user preferences file must be a regular file");
1✔
234
        }
7✔
235
        Ok(())
71✔
236
    }
79✔
237

238
    /// Creates a new local instance of the Trane given the path to the root of a course library.
239
    /// The user data will be stored in a directory named `.trane` inside the library root
240
    /// directory. The working directory will be used to resolve relative paths.
241
    pub fn new_local(working_dir: &Path, library_root: &Path) -> Result<Trane> {
79✔
242
        // Initialize the config directory.
79✔
243
        let config_path = library_root.join(Path::new(TRANE_CONFIG_DIR_PATH));
79✔
244
        Self::init_config_directory(library_root)?;
79✔
245

246
        // Build all the components needed to create a Trane instance.
247
        let preferences_manager = Arc::new(RwLock::new(LocalPreferencesManager {
71✔
248
            path: library_root
71✔
249
                .join(TRANE_CONFIG_DIR_PATH)
71✔
250
                .join(USER_PREFERENCES_PATH),
71✔
251
        }));
71✔
252
        let user_preferences = preferences_manager.read().get_user_preferences()?;
71✔
253
        let course_library = Arc::new(RwLock::new(LocalCourseLibrary::new(
71✔
254
            &working_dir.join(library_root),
71✔
255
            user_preferences.clone(),
71✔
256
        )?));
71✔
257
        let transcription_preferences = user_preferences.transcription.clone().unwrap_or_default();
71✔
258
        let transcription_downloader = Arc::new(RwLock::new(LocalTranscriptionDownloader {
71✔
259
            preferences: transcription_preferences,
71✔
260
            link_store: course_library.clone(),
71✔
261
        }));
71✔
262
        let unit_graph = course_library.write().get_unit_graph();
71✔
263
        let practice_stats = Arc::new(RwLock::new(LocalPracticeStats::new_from_disk(
71✔
264
            config_path.join(PRACTICE_STATS_PATH).to_str().unwrap(),
71✔
265
        )?));
71✔
266
        let blacklist = Arc::new(RwLock::new(LocalBlacklist::new_from_disk(
71✔
267
            config_path.join(BLACKLIST_PATH).to_str().unwrap(),
71✔
268
        )?));
71✔
269
        let review_list = Arc::new(RwLock::new(LocalReviewList::new_from_disk(
71✔
270
            config_path.join(REVIEW_LIST_PATH).to_str().unwrap(),
71✔
271
        )?));
71✔
272
        let filter_manager = Arc::new(RwLock::new(LocalFilterManager::new(
71✔
273
            config_path.join(FILTERS_DIR).to_str().unwrap(),
71✔
274
        )?));
71✔
275
        let study_sessions_manager = Arc::new(RwLock::new(LocalStudySessionManager::new(
71✔
276
            config_path.join(STUDY_SESSIONS_DIR).to_str().unwrap(),
71✔
277
        )?));
71✔
278
        let repo_manager = Arc::new(RwLock::new(LocalRepositoryManager::new(library_root)?));
71✔
279
        let mut mantra_miner = TraneMantraMiner::default();
71✔
280
        mantra_miner.mantra_miner.start()?;
71✔
281
        let options = Self::create_scheduler_options(user_preferences.scheduler.as_ref());
71✔
282
        options.verify()?;
71✔
283
        let scheduler_data = SchedulerData {
71✔
284
            options,
71✔
285
            course_library: course_library.clone(),
71✔
286
            unit_graph: unit_graph.clone(),
71✔
287
            practice_stats: practice_stats.clone(),
71✔
288
            blacklist: blacklist.clone(),
71✔
289
            review_list: review_list.clone(),
71✔
290
            filter_manager: filter_manager.clone(),
71✔
291
            frequency_map: Arc::new(RwLock::new(UstrMap::default())),
71✔
292
        };
71✔
293

71✔
294
        Ok(Trane {
71✔
295
            blacklist,
71✔
296
            course_library,
71✔
297
            filter_manager,
71✔
298
            library_root: library_root.to_str().unwrap().to_string(),
71✔
299
            practice_stats,
71✔
300
            preferences_manager,
71✔
301
            repo_manager,
71✔
302
            review_list,
71✔
303
            scheduler_data: scheduler_data.clone(),
71✔
304
            scheduler: DepthFirstScheduler::new(scheduler_data),
71✔
305
            study_session_manager: study_sessions_manager,
71✔
306
            unit_graph,
71✔
307
            transcription_downloader,
71✔
308
            mantra_miner,
71✔
309
        })
71✔
310
    }
79✔
311

312
    /// Returns the path to the root of the course library.
313
    pub fn library_root(&self) -> String {
1✔
314
        self.library_root.clone()
1✔
315
    }
1✔
316

317
    /// Returns the number of mantras that have been recited by the mantra miner.
318
    pub fn mantra_count(&self) -> usize {
1✔
319
        self.mantra_miner.mantra_miner.count()
1✔
320
    }
1✔
321

322
    /// Returns a clone of the data used by the scheduler. This function is needed by tests that
323
    /// need to verify internal methods.
324
    #[allow(dead_code)]
325
    fn get_scheduler_data(&self) -> SchedulerData {
11✔
326
        self.scheduler_data.clone()
11✔
327
    }
11✔
328
}
329

330
impl Blacklist for Trane {
331
    fn add_to_blacklist(&mut self, unit_id: Ustr) -> Result<(), BlacklistError> {
58✔
332
        // Make sure to invalidate any cached scores for the given unit.
58✔
333
        self.scheduler.invalidate_cached_score(unit_id);
58✔
334
        self.blacklist.write().add_to_blacklist(unit_id)
58✔
335
    }
58✔
336

337
    fn remove_from_blacklist(&mut self, unit_id: Ustr) -> Result<(), BlacklistError> {
20✔
338
        // Make sure to invalidate any cached scores for the given unit.
20✔
339
        self.scheduler.invalidate_cached_score(unit_id);
20✔
340
        self.blacklist.write().remove_from_blacklist(unit_id)
20✔
341
    }
20✔
342

UNCOV
343
    fn remove_prefix_from_blacklist(&mut self, prefix: &str) -> Result<(), BlacklistError> {
×
UNCOV
344
        // Make sure to invalidate any cached scores of units with the given prefix.
×
UNCOV
345
        self.scheduler.invalidate_cached_scores_with_prefix(prefix);
×
UNCOV
346
        self.blacklist.write().remove_prefix_from_blacklist(prefix)
×
UNCOV
347
    }
×
348

UNCOV
349
    fn blacklisted(&self, unit_id: Ustr) -> Result<bool, BlacklistError> {
×
UNCOV
350
        self.blacklist.read().blacklisted(unit_id)
×
UNCOV
351
    }
×
352

UNCOV
353
    fn get_blacklist_entries(&self) -> Result<Vec<Ustr>, BlacklistError> {
×
UNCOV
354
        self.blacklist.read().get_blacklist_entries()
×
UNCOV
355
    }
×
356
}
357

358
impl CourseLibrary for Trane {
UNCOV
359
    fn get_course_manifest(&self, course_id: Ustr) -> Option<CourseManifest> {
×
UNCOV
360
        self.course_library.read().get_course_manifest(course_id)
×
UNCOV
361
    }
×
362

UNCOV
363
    fn get_lesson_manifest(&self, lesson_id: Ustr) -> Option<LessonManifest> {
×
UNCOV
364
        self.course_library.read().get_lesson_manifest(lesson_id)
×
UNCOV
365
    }
×
366

UNCOV
367
    fn get_exercise_manifest(&self, exercise_id: Ustr) -> Option<ExerciseManifest> {
×
UNCOV
368
        self.course_library
×
UNCOV
369
            .read()
×
UNCOV
370
            .get_exercise_manifest(exercise_id)
×
UNCOV
371
    }
×
372

373
    fn get_course_ids(&self) -> Vec<Ustr> {
1✔
374
        self.course_library.read().get_course_ids()
1✔
375
    }
1✔
376

377
    fn get_lesson_ids(&self, course_id: Ustr) -> Option<Vec<Ustr>> {
8✔
378
        self.course_library.read().get_lesson_ids(course_id)
8✔
379
    }
8✔
380

381
    fn get_exercise_ids(&self, lesson_id: Ustr) -> Option<Vec<Ustr>> {
18✔
382
        self.course_library.read().get_exercise_ids(lesson_id)
18✔
383
    }
18✔
384

385
    fn get_all_exercise_ids(&self, unit_id: Option<Ustr>) -> Vec<Ustr> {
18✔
386
        self.course_library.read().get_all_exercise_ids(unit_id)
18✔
387
    }
18✔
388

389
    fn get_matching_prefix(&self, prefix: &str, unit_type: Option<UnitType>) -> UstrSet {
4✔
390
        self.course_library
4✔
391
            .read()
4✔
392
            .get_matching_prefix(prefix, unit_type)
4✔
393
    }
4✔
394

395
    fn search(&self, query: &str) -> Result<Vec<Ustr>, CourseLibraryError> {
11✔
396
        self.course_library.read().search(query)
11✔
397
    }
11✔
398
}
399

400
impl ExerciseScheduler for Trane {
401
    fn get_exercise_batch(
8,563✔
402
        &self,
8,563✔
403
        filter: Option<ExerciseFilter>,
8,563✔
404
    ) -> Result<Vec<ExerciseManifest>, ExerciseSchedulerError> {
8,563✔
405
        self.scheduler.get_exercise_batch(filter)
8,563✔
406
    }
8,563✔
407

408
    fn score_exercise(
79,474✔
409
        &self,
79,474✔
410
        exercise_id: Ustr,
79,474✔
411
        score: MasteryScore,
79,474✔
412
        timestamp: i64,
79,474✔
413
    ) -> Result<(), ExerciseSchedulerError> {
79,474✔
414
        self.scheduler.score_exercise(exercise_id, score, timestamp)
79,474✔
415
    }
79,474✔
416

UNCOV
417
    fn invalidate_cached_score(&self, unit_id: Ustr) {
×
UNCOV
418
        self.scheduler.invalidate_cached_score(unit_id);
×
UNCOV
419
    }
×
420

UNCOV
421
    fn invalidate_cached_scores_with_prefix(&self, prefix: &str) {
×
UNCOV
422
        self.scheduler.invalidate_cached_scores_with_prefix(prefix);
×
UNCOV
423
    }
×
424

425
    fn get_scheduler_options(&self) -> SchedulerOptions {
2✔
426
        self.scheduler.get_scheduler_options()
2✔
427
    }
2✔
428

429
    fn set_scheduler_options(&mut self, options: SchedulerOptions) {
2✔
430
        self.scheduler.set_scheduler_options(options);
2✔
431
    }
2✔
432

433
    fn reset_scheduler_options(&mut self) {
1✔
434
        self.scheduler.reset_scheduler_options();
1✔
435
    }
1✔
436
}
437

438
impl FilterManager for Trane {
UNCOV
439
    fn get_filter(&self, id: &str) -> Option<SavedFilter> {
×
UNCOV
440
        self.filter_manager.read().get_filter(id)
×
UNCOV
441
    }
×
442

UNCOV
443
    fn list_filters(&self) -> Vec<(String, String)> {
×
UNCOV
444
        self.filter_manager.read().list_filters()
×
UNCOV
445
    }
×
446
}
447

448
impl PracticeStats for Trane {
449
    fn get_scores(
2,646✔
450
        &self,
2,646✔
451
        exercise_id: Ustr,
2,646✔
452
        num_scores: usize,
2,646✔
453
    ) -> Result<Vec<ExerciseTrial>, PracticeStatsError> {
2,646✔
454
        self.practice_stats
2,646✔
455
            .read()
2,646✔
456
            .get_scores(exercise_id, num_scores)
2,646✔
457
    }
2,646✔
458

UNCOV
459
    fn record_exercise_score(
×
UNCOV
460
        &mut self,
×
UNCOV
461
        exercise_id: Ustr,
×
UNCOV
462
        score: MasteryScore,
×
UNCOV
463
        timestamp: i64,
×
UNCOV
464
    ) -> Result<(), PracticeStatsError> {
×
UNCOV
465
        self.practice_stats
×
UNCOV
466
            .write()
×
UNCOV
467
            .record_exercise_score(exercise_id, score, timestamp)
×
UNCOV
468
    }
×
469

UNCOV
470
    fn trim_scores(&mut self, num_scores: usize) -> Result<(), PracticeStatsError> {
×
UNCOV
471
        self.practice_stats.write().trim_scores(num_scores)
×
UNCOV
472
    }
×
473

UNCOV
474
    fn remove_scores_with_prefix(&mut self, prefix: &str) -> Result<(), PracticeStatsError> {
×
UNCOV
475
        self.practice_stats
×
UNCOV
476
            .write()
×
UNCOV
477
            .remove_scores_with_prefix(prefix)
×
UNCOV
478
    }
×
479
}
480

481
impl PreferencesManager for Trane {
UNCOV
482
    fn get_user_preferences(&self) -> Result<UserPreferences, PreferencesManagerError> {
×
UNCOV
483
        self.preferences_manager.read().get_user_preferences()
×
UNCOV
484
    }
×
485

UNCOV
486
    fn set_user_preferences(
×
UNCOV
487
        &mut self,
×
UNCOV
488
        preferences: UserPreferences,
×
UNCOV
489
    ) -> Result<(), PreferencesManagerError> {
×
UNCOV
490
        self.preferences_manager
×
UNCOV
491
            .write()
×
UNCOV
492
            .set_user_preferences(preferences)
×
UNCOV
493
    }
×
494
}
495

496
impl RepositoryManager for Trane {
UNCOV
497
    fn add_repo(
×
UNCOV
498
        &mut self,
×
UNCOV
499
        url: &str,
×
UNCOV
500
        repo_id: Option<String>,
×
UNCOV
501
    ) -> Result<(), RepositoryManagerError> {
×
UNCOV
502
        self.repo_manager.write().add_repo(url, repo_id)
×
UNCOV
503
    }
×
504

UNCOV
505
    fn remove_repo(&mut self, repo_id: &str) -> Result<(), RepositoryManagerError> {
×
UNCOV
506
        self.repo_manager.write().remove_repo(repo_id)
×
UNCOV
507
    }
×
508

UNCOV
509
    fn update_repo(&self, repo_id: &str) -> Result<(), RepositoryManagerError> {
×
UNCOV
510
        self.repo_manager.read().update_repo(repo_id)
×
UNCOV
511
    }
×
512

UNCOV
513
    fn update_all_repos(&self) -> Result<(), RepositoryManagerError> {
×
UNCOV
514
        self.repo_manager.read().update_all_repos()
×
UNCOV
515
    }
×
516

UNCOV
517
    fn list_repos(&self) -> Vec<RepositoryMetadata> {
×
UNCOV
518
        self.repo_manager.read().list_repos()
×
UNCOV
519
    }
×
520
}
521

522
impl ReviewList for Trane {
523
    fn add_to_review_list(&mut self, unit_id: Ustr) -> Result<(), ReviewListError> {
6✔
524
        self.review_list.write().add_to_review_list(unit_id)
6✔
525
    }
6✔
526

UNCOV
527
    fn remove_from_review_list(&mut self, unit_id: Ustr) -> Result<(), ReviewListError> {
×
UNCOV
528
        self.review_list.write().remove_from_review_list(unit_id)
×
UNCOV
529
    }
×
530

UNCOV
531
    fn get_review_list_entries(&self) -> Result<Vec<Ustr>, ReviewListError> {
×
UNCOV
532
        self.review_list.read().get_review_list_entries()
×
UNCOV
533
    }
×
534
}
535

536
impl StudySessionManager for Trane {
UNCOV
537
    fn get_study_session(&self, id: &str) -> Option<data::filter::StudySession> {
×
UNCOV
538
        self.study_session_manager.read().get_study_session(id)
×
UNCOV
539
    }
×
540

UNCOV
541
    fn list_study_sessions(&self) -> Vec<(String, String)> {
×
UNCOV
542
        self.study_session_manager.read().list_study_sessions()
×
UNCOV
543
    }
×
544
}
545

546
impl TranscriptionDownloader for Trane {
UNCOV
547
    fn is_transcription_asset_downloaded(&self, exercise_id: Ustr) -> bool {
×
UNCOV
548
        self.transcription_downloader
×
UNCOV
549
            .read()
×
UNCOV
550
            .is_transcription_asset_downloaded(exercise_id)
×
UNCOV
551
    }
×
552

UNCOV
553
    fn download_transcription_asset(
×
UNCOV
554
        &self,
×
UNCOV
555
        exercise_id: Ustr,
×
UNCOV
556
        force_download: bool,
×
UNCOV
557
    ) -> Result<(), TranscriptionDownloaderError> {
×
UNCOV
558
        self.transcription_downloader
×
UNCOV
559
            .write()
×
UNCOV
560
            .download_transcription_asset(exercise_id, force_download)
×
UNCOV
561
    }
×
562

UNCOV
563
    fn transcription_download_path(&self, exercise_id: Ustr) -> Option<std::path::PathBuf> {
×
UNCOV
564
        self.transcription_downloader
×
UNCOV
565
            .read()
×
UNCOV
566
            .transcription_download_path(exercise_id)
×
UNCOV
567
    }
×
568

UNCOV
569
    fn transcription_download_path_alias(&self, exercise_id: Ustr) -> Option<std::path::PathBuf> {
×
UNCOV
570
        self.transcription_downloader
×
UNCOV
571
            .read()
×
UNCOV
572
            .transcription_download_path_alias(exercise_id)
×
UNCOV
573
    }
×
574
}
575

576
impl UnitGraph for Trane {
UNCOV
577
    fn add_course(&mut self, course_id: Ustr) -> Result<(), UnitGraphError> {
×
UNCOV
578
        self.unit_graph.write().add_course(course_id)
×
UNCOV
579
    }
×
580

UNCOV
581
    fn add_lesson(&mut self, lesson_id: Ustr, course_id: Ustr) -> Result<(), UnitGraphError> {
×
UNCOV
582
        self.unit_graph.write().add_lesson(lesson_id, course_id)
×
UNCOV
583
    }
×
584

UNCOV
585
    fn add_exercise(&mut self, exercise_id: Ustr, lesson_id: Ustr) -> Result<(), UnitGraphError> {
×
UNCOV
586
        self.unit_graph.write().add_exercise(exercise_id, lesson_id)
×
UNCOV
587
    }
×
588

UNCOV
589
    fn add_dependencies(
×
UNCOV
590
        &mut self,
×
UNCOV
591
        unit_id: Ustr,
×
UNCOV
592
        unit_type: UnitType,
×
UNCOV
593
        dependencies: &[Ustr],
×
UNCOV
594
    ) -> Result<(), UnitGraphError> {
×
UNCOV
595
        self.unit_graph
×
UNCOV
596
            .write()
×
UNCOV
597
            .add_dependencies(unit_id, unit_type, dependencies)
×
UNCOV
598
    }
×
599

UNCOV
600
    fn add_superseded(&mut self, unit_id: Ustr, superseded: &[Ustr]) {
×
UNCOV
601
        self.unit_graph.write().add_superseded(unit_id, superseded);
×
UNCOV
602
    }
×
603

UNCOV
604
    fn get_unit_type(&self, unit_id: Ustr) -> Option<UnitType> {
×
UNCOV
605
        self.unit_graph.read().get_unit_type(unit_id)
×
UNCOV
606
    }
×
607

UNCOV
608
    fn get_course_lessons(&self, course_id: Ustr) -> Option<UstrSet> {
×
UNCOV
609
        self.unit_graph.read().get_course_lessons(course_id)
×
UNCOV
610
    }
×
611

UNCOV
612
    fn get_starting_lessons(&self, course_id: Ustr) -> Option<UstrSet> {
×
UNCOV
613
        self.unit_graph.read().get_starting_lessons(course_id)
×
UNCOV
614
    }
×
615

UNCOV
616
    fn update_starting_lessons(&mut self) {
×
UNCOV
617
        self.unit_graph.write().update_starting_lessons();
×
UNCOV
618
    }
×
619

UNCOV
620
    fn get_lesson_course(&self, lesson_id: Ustr) -> Option<Ustr> {
×
UNCOV
621
        self.unit_graph.read().get_lesson_course(lesson_id)
×
UNCOV
622
    }
×
623

UNCOV
624
    fn get_lesson_exercises(&self, lesson_id: Ustr) -> Option<UstrSet> {
×
UNCOV
625
        self.unit_graph.read().get_lesson_exercises(lesson_id)
×
UNCOV
626
    }
×
627

UNCOV
628
    fn get_exercise_lesson(&self, exercise_id: Ustr) -> Option<Ustr> {
×
UNCOV
629
        self.unit_graph.read().get_exercise_lesson(exercise_id)
×
UNCOV
630
    }
×
631

UNCOV
632
    fn get_dependencies(&self, unit_id: Ustr) -> Option<UstrSet> {
×
UNCOV
633
        self.unit_graph.read().get_dependencies(unit_id)
×
UNCOV
634
    }
×
635

UNCOV
636
    fn get_dependents(&self, unit_id: Ustr) -> Option<UstrSet> {
×
UNCOV
637
        self.unit_graph.read().get_dependents(unit_id)
×
UNCOV
638
    }
×
639

UNCOV
640
    fn get_dependency_sinks(&self) -> UstrSet {
×
UNCOV
641
        self.unit_graph.read().get_dependency_sinks()
×
UNCOV
642
    }
×
643

UNCOV
644
    fn get_superseded(&self, unit_id: Ustr) -> Option<UstrSet> {
×
UNCOV
645
        self.unit_graph.read().get_superseded(unit_id)
×
UNCOV
646
    }
×
647

UNCOV
648
    fn get_superseding(&self, unit_id: Ustr) -> Option<UstrSet> {
×
UNCOV
649
        self.unit_graph.read().get_superseding(unit_id)
×
UNCOV
650
    }
×
651

UNCOV
652
    fn check_cycles(&self) -> Result<(), UnitGraphError> {
×
UNCOV
653
        self.unit_graph.read().check_cycles()
×
UNCOV
654
    }
×
655

UNCOV
656
    fn generate_dot_graph(&self) -> String {
×
UNCOV
657
        self.unit_graph.read().generate_dot_graph()
×
UNCOV
658
    }
×
659
}
660

661
#[cfg(test)]
662
mod test {
663
    use anyhow::Result;
664
    use std::{fs::*, os::unix::prelude::PermissionsExt, thread, time::Duration};
665

666
    use crate::{
667
        data::{SchedulerOptions, SchedulerPreferences, UserPreferences},
668
        Trane, FILTERS_DIR, STUDY_SESSIONS_DIR, TRANE_CONFIG_DIR_PATH, USER_PREFERENCES_PATH,
669
    };
670

671
    /// Verifies retrieving the root of a library.
672
    #[test]
673
    fn library_root() -> Result<()> {
1✔
674
        let dir = tempfile::tempdir()?;
1✔
675
        let trane = Trane::new_local(dir.path(), dir.path())?;
1✔
676
        assert_eq!(trane.library_root(), dir.path().to_str().unwrap());
1✔
677
        Ok(())
1✔
678
    }
1✔
679

680
    /// Verifies that the mantra-miner starts up and has a valid count.
681
    #[test]
682
    fn mantra_count() -> Result<()> {
1✔
683
        let dir = tempfile::tempdir()?;
1✔
684
        let trane = Trane::new_local(dir.path(), dir.path())?;
1✔
685
        thread::sleep(Duration::from_millis(1000));
1✔
686
        assert!(trane.mantra_count() > 0);
1✔
687
        Ok(())
1✔
688
    }
1✔
689

690
    /// Verifies opening a course library with a path that is not a directory fails.
691
    #[test]
692
    fn library_root_is_not_dir() -> Result<()> {
1✔
693
        let file = tempfile::NamedTempFile::new()?;
1✔
694
        let result = Trane::new_local(file.path(), file.path());
1✔
695
        assert!(result.is_err());
1✔
696
        Ok(())
1✔
697
    }
1✔
698

699
    /// Verifies that opening a library if the path to the config directory exists but is not a
700
    /// directory.
701
    #[test]
702
    fn config_dir_is_file() -> Result<()> {
1✔
703
        let dir = tempfile::tempdir()?;
1✔
704
        let trane_path = dir.path().join(".trane");
1✔
705
        File::create(trane_path)?;
1✔
706
        assert!(Trane::new_local(dir.path(), dir.path()).is_err());
1✔
707
        Ok(())
1✔
708
    }
1✔
709

710
    /// Verifies that opening a library fails if the directory has bad permissions.
711
    #[test]
712
    fn bad_dir_permissions() -> Result<()> {
1✔
713
        let dir = tempfile::tempdir()?;
1✔
714
        set_permissions(&dir, Permissions::from_mode(0o000))?;
1✔
715
        assert!(Trane::new_local(dir.path(), dir.path()).is_err());
1✔
716
        Ok(())
1✔
717
    }
1✔
718

719
    /// Verifies that opening a library fails if the config directory has bad permissions.
720
    #[test]
721
    fn bad_config_dir_permissions() -> Result<()> {
1✔
722
        let dir = tempfile::tempdir()?;
1✔
723
        let config_dir_path = dir.path().join(".trane");
1✔
724
        create_dir(&config_dir_path)?;
1✔
725
        set_permissions(&config_dir_path, Permissions::from_mode(0o000))?;
1✔
726
        assert!(Trane::new_local(dir.path(), dir.path()).is_err());
1✔
727
        Ok(())
1✔
728
    }
1✔
729

730
    /// Verifies that opening a library fails if the user preferences file is not a file.
731
    #[test]
732
    fn user_preferences_file_is_a_dir() -> Result<()> {
1✔
733
        // Create directory `./trane/user_preferences.json` which is not a file.
734
        let temp_dir = tempfile::tempdir()?;
1✔
735
        std::fs::create_dir_all(
1✔
736
            temp_dir
1✔
737
                .path()
1✔
738
                .join(TRANE_CONFIG_DIR_PATH)
1✔
739
                .join(USER_PREFERENCES_PATH),
1✔
740
        )?;
1✔
741
        assert!(Trane::new_local(temp_dir.path(), temp_dir.path()).is_err());
1✔
742
        Ok(())
1✔
743
    }
1✔
744

745
    /// Verifies that opening a library fails if the filters directory cannot be created.
746
    #[test]
747
    fn cannot_create_filters_directory() -> Result<()> {
1✔
748
        // Create config directory.
749
        let temp_dir = tempfile::tempdir()?;
1✔
750
        let config_dir = temp_dir.path().join(TRANE_CONFIG_DIR_PATH);
1✔
751
        create_dir(config_dir.clone())?;
1✔
752

753
        // Set permissions of `.trane` directory to read-only.
754
        std::fs::set_permissions(temp_dir.path(), std::fs::Permissions::from_mode(0o444))?;
1✔
755

756
        assert!(Trane::new_local(temp_dir.path(), temp_dir.path()).is_err());
1✔
757
        Ok(())
1✔
758
    }
1✔
759

760
    /// Verifies that opening a library fails if the study sessions directory cannot be created.
761
    #[test]
762
    fn cannot_create_study_sessions() -> Result<()> {
1✔
763
        // Create config and filters directories.
764
        let temp_dir = tempfile::tempdir()?;
1✔
765
        let config_dir = temp_dir.path().join(TRANE_CONFIG_DIR_PATH);
1✔
766
        create_dir(config_dir.clone())?;
1✔
767
        let filters_dir = config_dir.join(FILTERS_DIR);
1✔
768
        create_dir(filters_dir)?;
1✔
769

770
        // Set permissions of `.trane` directory to read-only. This should prevent Trane from
771
        // creating the user preferences file.
772
        std::fs::set_permissions(config_dir, std::fs::Permissions::from_mode(0o500))?;
1✔
773
        assert!(Trane::new_local(temp_dir.path(), temp_dir.path()).is_err());
1✔
774
        Ok(())
1✔
775
    }
1✔
776

777
    /// Verifies that opening a library fails if the user preferences file cannot be created.
778
    #[test]
779
    fn cannot_create_user_preferences() -> Result<()> {
1✔
780
        // Create config, filters, and study sessions directories.
781
        let temp_dir = tempfile::tempdir()?;
1✔
782
        let config_dir = temp_dir.path().join(TRANE_CONFIG_DIR_PATH);
1✔
783
        create_dir(config_dir.clone())?;
1✔
784
        let filters_dir = config_dir.join(FILTERS_DIR);
1✔
785
        create_dir(filters_dir)?;
1✔
786
        let sessions_dir = config_dir.join(STUDY_SESSIONS_DIR);
1✔
787
        create_dir(sessions_dir)?;
1✔
788

789
        // Set permissions of `.trane` directory to read-only. This should prevent Trane from
790
        // creating the user preferences file.
791
        std::fs::set_permissions(config_dir, std::fs::Permissions::from_mode(0o500))?;
1✔
792
        assert!(Trane::new_local(temp_dir.path(), temp_dir.path()).is_err());
1✔
793
        Ok(())
1✔
794
    }
1✔
795

796
    /// Verifies retrieving the scheduler data from the library.
797
    #[test]
798
    fn scheduler_data() -> Result<()> {
1✔
799
        let dir = tempfile::tempdir()?;
1✔
800
        let trane = Trane::new_local(dir.path(), dir.path())?;
1✔
801
        trane.get_scheduler_data();
1✔
802
        Ok(())
1✔
803
    }
1✔
804

805
    /// Verifies building the scheduler options from the user preferences.
806
    #[test]
807
    fn scheduler_options() {
1✔
808
        // Test with no preferences.
1✔
809
        let user_preferences = UserPreferences {
1✔
810
            scheduler: None,
1✔
811
            transcription: None,
1✔
812
            ignored_paths: vec![],
1✔
813
        };
1✔
814
        let options = Trane::create_scheduler_options(user_preferences.scheduler.as_ref());
1✔
815
        assert_eq!(options.batch_size, SchedulerOptions::default().batch_size);
1✔
816

817
        // Test with preferences.
818
        let user_preferences = UserPreferences {
1✔
819
            scheduler: Some(SchedulerPreferences {
1✔
820
                batch_size: Some(10),
1✔
821
            }),
1✔
822
            transcription: None,
1✔
823
            ignored_paths: vec![],
1✔
824
        };
1✔
825
        let options = Trane::create_scheduler_options(user_preferences.scheduler.as_ref());
1✔
826
        assert_eq!(options.batch_size, 10);
1✔
827
    }
1✔
828
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc