• 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

99.14
/src/data/course_generator/literacy.rs
1
//! Defines a special course to teach literacy skills.
2
//!
3
//! The student is presented with examples and exceptions that match a certain spelling rule or type
4
//! of reading material. They are asked to read the example and exceptions and are scored based on
5
//! how many they get right. Optionally, a dictation lesson can be generated where the student is
6
//! asked to write the examples and exceptions based on the tutor's dictation.
7

8
use anyhow::{Context, Result};
9
use serde::{Deserialize, Serialize};
10
use std::{collections::BTreeMap, fs, path::Path};
11
use ts_rs::TS;
12

13
use crate::data::{
14
    CourseGenerator, CourseManifest, ExerciseAsset, ExerciseManifest, ExerciseType,
15
    GenerateManifests, GeneratedCourse, LessonManifest, UserPreferences,
16
};
17

18
/// The metadata key indicating this is a literacy course. Its value should be set to "true".
19
pub const COURSE_METADATA: &str = "literacy_course";
20

21
/// The metadata indicating the type of literacy lesson.
22
pub const LESSON_METADATA: &str = "literacy_lesson";
23

24
/// The extension of files containing examples.
25
pub const EXAMPLE_SUFFIX: &str = ".example.md";
26

27
/// The extension of files containing exceptions.
28
pub const EXCEPTION_SUFFIX: &str = ".exception.md";
29

30
/// The types of literacy lessons that can be generated.
31
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
31✔
32
#[ts(export)]
33
pub enum LiteracyLesson {
34
    /// A lesson that takes examples and exceptions and asks the student to read them.
35
    Reading,
36

37
    /// A lesson that takes examples and exceptions and asks the student to write them based on the
38
    /// tutor's dictation.
39
    Dictation,
40
}
41

42
/// The configuration to create a course that teaches literacy based on the provided material.
43
/// Material can be of two types.
44
///
45
/// 1. Examples. For example, they can be words that share the same spelling and pronunciation (e.g.
46
///    "cat", "bat", "hat"), sentences that share similar words, or sentences from the same book or
47
///    article (for more advanced courses).
48
/// 2. Exceptions. For example, they can be words that share the same spelling but have different
49
///    pronunciations (e.g. "cow" and "crow").
50
///
51
/// All examples and exceptions accept Markdown syntax. Examples and exceptions can be declared in
52
/// the configuration or in separate files in the course's directory. Files that end with the
53
/// extensions ".examples.md" and ".exceptions.md" will be considered as examples and exceptions,
54
/// respectively.
55
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, TS)]
31✔
56
#[ts(export)]
57
pub struct LiteracyConfig {
58
    /// Inlined examples to use in the course.
59
    #[serde(default)]
60
    inlined_examples: Vec<String>,
61

62
    /// Inlined exceptions to use in the course.
63
    #[serde(default)]
64
    inlined_exceptions: Vec<String>,
65

66
    /// Indicates whether to generate an optional lesson that asks the student to write the material
67
    /// based on the tutor's dictation.
68
    #[serde(default)]
69
    pub generate_dictation: bool,
70
}
71

72
impl LiteracyConfig {
73
    fn generate_reading_lesson(
2✔
74
        course_manifest: &CourseManifest,
2✔
75
        examples: &[String],
2✔
76
        exceptions: &[String],
2✔
77
    ) -> (LessonManifest, Vec<ExerciseManifest>) {
2✔
78
        // Create the lesson manifest.
2✔
79
        let lesson_manifest = LessonManifest {
2✔
80
            id: format!("{}::reading", course_manifest.id).into(),
2✔
81
            dependencies: vec![],
2✔
82
            superseded: vec![],
2✔
83
            course_id: course_manifest.id,
2✔
84
            name: format!("{} - Reading", course_manifest.name),
2✔
85
            description: None,
2✔
86
            metadata: Some(BTreeMap::from([(
2✔
87
                LESSON_METADATA.to_string(),
2✔
88
                vec!["reading".to_string()],
2✔
89
            )])),
2✔
90
            lesson_material: None,
2✔
91
            lesson_instructions: None,
2✔
92
        };
2✔
93

2✔
94
        // Create the exercise manifest.
2✔
95
        let exercise_manifest = ExerciseManifest {
2✔
96
            id: format!("{}::reading::exercise", course_manifest.id).into(),
2✔
97
            lesson_id: lesson_manifest.id,
2✔
98
            course_id: course_manifest.id,
2✔
99
            name: format!("{} - Reading", course_manifest.name),
2✔
100
            description: None,
2✔
101
            exercise_type: ExerciseType::Procedural,
2✔
102
            exercise_asset: ExerciseAsset::LiteracyAsset {
2✔
103
                lesson_type: LiteracyLesson::Reading,
2✔
104
                examples: examples.to_vec(),
2✔
105
                exceptions: exceptions.to_vec(),
2✔
106
            },
2✔
107
        };
2✔
108
        (lesson_manifest, vec![exercise_manifest])
2✔
109
    }
2✔
110

111
    fn generate_dictation_lesson(
2✔
112
        course_manifest: &CourseManifest,
2✔
113
        examples: &[String],
2✔
114
        exceptions: &[String],
2✔
115
    ) -> Option<(LessonManifest, Vec<ExerciseManifest>)> {
2✔
116
        // Exit early if the dictation lesson should not be generated.
117
        let generate_dictation =
2✔
118
            if let Some(CourseGenerator::Literacy(config)) = &course_manifest.generator_config {
2✔
119
                config.generate_dictation
2✔
120
            } else {
NEW
121
                false
×
122
            };
123
        if !generate_dictation {
2✔
124
            return None;
1✔
125
        }
1✔
126

1✔
127
        // Create the lesson manifest.
1✔
128
        let lesson_manifest = LessonManifest {
1✔
129
            id: format!("{}::dictation", course_manifest.id).into(),
1✔
130
            dependencies: vec![format!("{}::reading", course_manifest.id).into()],
1✔
131
            superseded: vec![],
1✔
132
            course_id: course_manifest.id,
1✔
133
            name: format!("{} - Dictation", course_manifest.name),
1✔
134
            description: None,
1✔
135
            metadata: Some(BTreeMap::from([(
1✔
136
                LESSON_METADATA.to_string(),
1✔
137
                vec!["dictation".to_string()],
1✔
138
            )])),
1✔
139
            lesson_material: None,
1✔
140
            lesson_instructions: None,
1✔
141
        };
1✔
142

1✔
143
        // Create the exercise manifest.
1✔
144
        let exercise_manifest = ExerciseManifest {
1✔
145
            id: format!("{}::dictation::exercise", course_manifest.id).into(),
1✔
146
            lesson_id: lesson_manifest.id,
1✔
147
            course_id: course_manifest.id,
1✔
148
            name: format!("{} - Dictation", course_manifest.name),
1✔
149
            description: None,
1✔
150
            exercise_type: ExerciseType::Procedural,
1✔
151
            exercise_asset: ExerciseAsset::LiteracyAsset {
1✔
152
                lesson_type: LiteracyLesson::Dictation,
1✔
153
                examples: examples.to_vec(),
1✔
154
                exceptions: exceptions.to_vec(),
1✔
155
            },
1✔
156
        };
1✔
157
        Some((lesson_manifest, vec![exercise_manifest]))
1✔
158
    }
2✔
159

160
    /// Generates the reading lesson and the optional dictation lesson.
161
    fn generate_lessons(
2✔
162
        course_manifest: &CourseManifest,
2✔
163
        examples: &[String],
2✔
164
        exceptions: &[String],
2✔
165
    ) -> Vec<(LessonManifest, Vec<ExerciseManifest>)> {
2✔
166
        if let Some(lesson) = Self::generate_dictation_lesson(course_manifest, examples, exceptions)
2✔
167
        {
168
            vec![
1✔
169
                Self::generate_reading_lesson(course_manifest, examples, exceptions),
1✔
170
                lesson,
1✔
171
            ]
1✔
172
        } else {
173
            vec![Self::generate_reading_lesson(
1✔
174
                course_manifest,
1✔
175
                examples,
1✔
176
                exceptions,
1✔
177
            )]
1✔
178
        }
179
    }
2✔
180
}
181

182
impl GenerateManifests for LiteracyConfig {
183
    fn generate_manifests(
2✔
184
        &self,
2✔
185
        course_root: &Path,
2✔
186
        course_manifest: &CourseManifest,
2✔
187
        _preferences: &UserPreferences,
2✔
188
    ) -> Result<GeneratedCourse> {
2✔
189
        // Collect all the examples and exceptions. First, gather the inlined ones. Then, gather the
2✔
190
        // examples and exceptions from the files in the courses's root directory.
2✔
191
        let mut examples = self.inlined_examples.clone();
2✔
192
        let mut exceptions = self.inlined_exceptions.clone();
2✔
193
        for entry in fs::read_dir(course_root)? {
8✔
194
            // Ignore entries that are not a file.
195
            let entry = entry.context("Failed to read entry when generating literacy course")?;
8✔
196
            let path = entry.path();
8✔
197
            if !path.is_file() {
8✔
UNCOV
198
                continue;
×
199
            }
8✔
200

8✔
201
            // Check that the file name ends is either an example or exception file.
8✔
202
            let file_name = path.file_name().unwrap_or_default().to_str().unwrap();
8✔
203
            if file_name.ends_with(EXAMPLE_SUFFIX) {
8✔
204
                let example = fs::read_to_string(&path).context("Failed to read example file")?;
4✔
205
                examples.push(example);
4✔
206
            } else if file_name.ends_with(EXCEPTION_SUFFIX) {
4✔
207
                let exception =
4✔
208
                    fs::read_to_string(&path).context("Failed to read exception file")?;
4✔
209
                exceptions.push(exception);
4✔
UNCOV
210
            }
×
211
        }
212

213
        // Sort the lists to have predictable outputs.
214
        examples.sort();
2✔
215
        exceptions.sort();
2✔
216

2✔
217
        // Generate the manifests for all the lessons and exercises and metadata to indicate this is
2✔
218
        // a literacy course.
2✔
219
        let lessons = Self::generate_lessons(course_manifest, &examples, &exceptions);
2✔
220
        let mut metadata = course_manifest.metadata.clone().unwrap_or_default();
2✔
221
        metadata.insert(COURSE_METADATA.to_string(), vec!["true".to_string()]);
2✔
222
        Ok(GeneratedCourse {
2✔
223
            lessons,
2✔
224
            updated_metadata: Some(metadata),
2✔
225
            updated_instructions: None,
2✔
226
        })
2✔
227
    }
2✔
228
}
229

230
#[cfg(test)]
231
mod test {
232
    use anyhow::Result;
233
    use std::{collections::BTreeMap, fs, path::Path};
234

235
    use crate::data::{
236
        course_generator::literacy::{LiteracyConfig, LiteracyLesson},
237
        CourseGenerator, CourseManifest, ExerciseAsset, ExerciseManifest, ExerciseType,
238
        GenerateManifests, GeneratedCourse, LessonManifest, UserPreferences,
239
    };
240

241
    /// Writes the given number of example and exception files to the given directory.
242
    fn generate_test_files(root_dir: &Path, num_examples: u8, num_exceptions: u8) -> Result<()> {
2✔
243
        for i in 0..num_examples {
4✔
244
            let example_file = root_dir.join(format!("example_{i}.example.md"));
4✔
245
            let example_content = format!("example_{i}");
4✔
246
            fs::write(&example_file, example_content)?;
4✔
247
        }
248
        for i in 0..num_exceptions {
4✔
249
            let exception_file = root_dir.join(format!("exception_{i}.exception.md"));
4✔
250
            let exception_content = format!("exception_{i}");
4✔
251
            fs::write(&exception_file, exception_content)?;
4✔
252
        }
253
        Ok(())
2✔
254
    }
2✔
255

256
    /// Verifies generating a literacy course with a dictation lesson.
257
    #[test]
258
    fn test_generate_manifests_dictation() -> Result<()> {
1✔
259
        // Create course manifest and files.
1✔
260
        let config = CourseGenerator::Literacy(LiteracyConfig {
1✔
261
            generate_dictation: true,
1✔
262
            inlined_examples: vec![
1✔
263
                "inlined_example_0".to_string(),
1✔
264
                "inlined_example_1".to_string(),
1✔
265
            ],
1✔
266
            inlined_exceptions: vec![
1✔
267
                "inlined_exception_0".to_string(),
1✔
268
                "inlined_exception_1".to_string(),
1✔
269
            ],
1✔
270
        });
1✔
271
        let course_manifest = CourseManifest {
1✔
272
            id: "literacy_course".into(),
1✔
273
            name: "Literacy Course".into(),
1✔
274
            dependencies: vec![],
1✔
275
            superseded: vec![],
1✔
276
            description: None,
1✔
277
            authors: None,
1✔
278
            metadata: None,
1✔
279
            course_material: None,
1✔
280
            course_instructions: None,
1✔
281
            generator_config: Some(config.clone()),
1✔
282
        };
1✔
283
        let temp_dir = tempfile::tempdir()?;
1✔
284
        generate_test_files(temp_dir.path(), 2, 2)?;
1✔
285

286
        // Generate the manifests.
287
        let prefs = UserPreferences::default();
1✔
288
        let got = config.generate_manifests(temp_dir.path(), &course_manifest, &prefs)?;
1✔
289
        let want = GeneratedCourse {
1✔
290
            lessons: vec![
1✔
291
                (
1✔
292
                    LessonManifest {
1✔
293
                        id: "literacy_course::reading".into(),
1✔
294
                        dependencies: vec![],
1✔
295
                        superseded: vec![],
1✔
296
                        course_id: "literacy_course".into(),
1✔
297
                        name: "Literacy Course - Reading".into(),
1✔
298
                        description: None,
1✔
299
                        metadata: Some(BTreeMap::from([(
1✔
300
                            "literacy_lesson".to_string(),
1✔
301
                            vec!["reading".to_string()],
1✔
302
                        )])),
1✔
303
                        lesson_material: None,
1✔
304
                        lesson_instructions: None,
1✔
305
                    },
1✔
306
                    vec![ExerciseManifest {
1✔
307
                        id: "literacy_course::reading::exercise".into(),
1✔
308
                        lesson_id: "literacy_course::reading".into(),
1✔
309
                        course_id: "literacy_course".into(),
1✔
310
                        name: "Literacy Course - Reading".into(),
1✔
311
                        description: None,
1✔
312
                        exercise_type: ExerciseType::Procedural,
1✔
313
                        exercise_asset: ExerciseAsset::LiteracyAsset {
1✔
314
                            lesson_type: LiteracyLesson::Reading,
1✔
315
                            examples: vec![
1✔
316
                                "example_0".to_string(),
1✔
317
                                "example_1".to_string(),
1✔
318
                                "inlined_example_0".to_string(),
1✔
319
                                "inlined_example_1".to_string(),
1✔
320
                            ],
1✔
321
                            exceptions: vec![
1✔
322
                                "exception_0".to_string(),
1✔
323
                                "exception_1".to_string(),
1✔
324
                                "inlined_exception_0".to_string(),
1✔
325
                                "inlined_exception_1".to_string(),
1✔
326
                            ],
1✔
327
                        },
1✔
328
                    }],
1✔
329
                ),
1✔
330
                (
1✔
331
                    LessonManifest {
1✔
332
                        id: "literacy_course::dictation".into(),
1✔
333
                        dependencies: vec!["literacy_course::reading".into()],
1✔
334
                        superseded: vec![],
1✔
335
                        course_id: "literacy_course".into(),
1✔
336
                        name: "Literacy Course - Dictation".into(),
1✔
337
                        description: None,
1✔
338
                        metadata: Some(BTreeMap::from([(
1✔
339
                            "literacy_lesson".to_string(),
1✔
340
                            vec!["dictation".to_string()],
1✔
341
                        )])),
1✔
342
                        lesson_material: None,
1✔
343
                        lesson_instructions: None,
1✔
344
                    },
1✔
345
                    vec![ExerciseManifest {
1✔
346
                        id: "literacy_course::dictation::exercise".into(),
1✔
347
                        lesson_id: "literacy_course::dictation".into(),
1✔
348
                        course_id: "literacy_course".into(),
1✔
349
                        name: "Literacy Course - Dictation".into(),
1✔
350
                        description: None,
1✔
351
                        exercise_type: ExerciseType::Procedural,
1✔
352
                        exercise_asset: ExerciseAsset::LiteracyAsset {
1✔
353
                            lesson_type: LiteracyLesson::Dictation,
1✔
354
                            examples: vec![
1✔
355
                                "example_0".to_string(),
1✔
356
                                "example_1".to_string(),
1✔
357
                                "inlined_example_0".to_string(),
1✔
358
                                "inlined_example_1".to_string(),
1✔
359
                            ],
1✔
360
                            exceptions: vec![
1✔
361
                                "exception_0".to_string(),
1✔
362
                                "exception_1".to_string(),
1✔
363
                                "inlined_exception_0".to_string(),
1✔
364
                                "inlined_exception_1".to_string(),
1✔
365
                            ],
1✔
366
                        },
1✔
367
                    }],
1✔
368
                ),
1✔
369
            ],
1✔
370
            updated_metadata: Some(BTreeMap::from([(
1✔
371
                "literacy_course".to_string(),
1✔
372
                vec!["true".to_string()],
1✔
373
            )])),
1✔
374
            updated_instructions: None,
1✔
375
        };
1✔
376
        assert_eq!(got, want);
1✔
377
        Ok(())
1✔
378
    }
1✔
379

380
    /// Verifies generating a literacy course with no dictation lesson.
381
    #[test]
382
    fn test_generate_manifests_no_dictation() -> Result<()> {
1✔
383
        // Create course manifest and files.
1✔
384
        let config = CourseGenerator::Literacy(LiteracyConfig {
1✔
385
            generate_dictation: false,
1✔
386
            inlined_examples: vec![
1✔
387
                "inlined_example_0".to_string(),
1✔
388
                "inlined_example_1".to_string(),
1✔
389
            ],
1✔
390
            inlined_exceptions: vec![
1✔
391
                "inlined_exception_0".to_string(),
1✔
392
                "inlined_exception_1".to_string(),
1✔
393
            ],
1✔
394
        });
1✔
395
        let course_manifest = CourseManifest {
1✔
396
            id: "literacy_course".into(),
1✔
397
            name: "Literacy Course".into(),
1✔
398
            dependencies: vec![],
1✔
399
            superseded: vec![],
1✔
400
            description: None,
1✔
401
            authors: None,
1✔
402
            metadata: None,
1✔
403
            course_material: None,
1✔
404
            course_instructions: None,
1✔
405
            generator_config: Some(config.clone()),
1✔
406
        };
1✔
407
        let temp_dir = tempfile::tempdir()?;
1✔
408
        generate_test_files(temp_dir.path(), 2, 2)?;
1✔
409

410
        // Generate the manifests.
411
        let prefs = UserPreferences::default();
1✔
412
        let got = config.generate_manifests(temp_dir.path(), &course_manifest, &prefs)?;
1✔
413
        let want = GeneratedCourse {
1✔
414
            lessons: vec![(
1✔
415
                LessonManifest {
1✔
416
                    id: "literacy_course::reading".into(),
1✔
417
                    dependencies: vec![],
1✔
418
                    superseded: vec![],
1✔
419
                    course_id: "literacy_course".into(),
1✔
420
                    name: "Literacy Course - Reading".into(),
1✔
421
                    description: None,
1✔
422
                    metadata: Some(BTreeMap::from([(
1✔
423
                        "literacy_lesson".to_string(),
1✔
424
                        vec!["reading".to_string()],
1✔
425
                    )])),
1✔
426
                    lesson_material: None,
1✔
427
                    lesson_instructions: None,
1✔
428
                },
1✔
429
                vec![ExerciseManifest {
1✔
430
                    id: "literacy_course::reading::exercise".into(),
1✔
431
                    lesson_id: "literacy_course::reading".into(),
1✔
432
                    course_id: "literacy_course".into(),
1✔
433
                    name: "Literacy Course - Reading".into(),
1✔
434
                    description: None,
1✔
435
                    exercise_type: ExerciseType::Procedural,
1✔
436
                    exercise_asset: ExerciseAsset::LiteracyAsset {
1✔
437
                        lesson_type: LiteracyLesson::Reading,
1✔
438
                        examples: vec![
1✔
439
                            "example_0".to_string(),
1✔
440
                            "example_1".to_string(),
1✔
441
                            "inlined_example_0".to_string(),
1✔
442
                            "inlined_example_1".to_string(),
1✔
443
                        ],
1✔
444
                        exceptions: vec![
1✔
445
                            "exception_0".to_string(),
1✔
446
                            "exception_1".to_string(),
1✔
447
                            "inlined_exception_0".to_string(),
1✔
448
                            "inlined_exception_1".to_string(),
1✔
449
                        ],
1✔
450
                    },
1✔
451
                }],
1✔
452
            )],
1✔
453
            updated_metadata: Some(BTreeMap::from([(
1✔
454
                "literacy_course".to_string(),
1✔
455
                vec!["true".to_string()],
1✔
456
            )])),
1✔
457
            updated_instructions: None,
1✔
458
        };
1✔
459
        assert_eq!(got, want);
1✔
460
        Ok(())
1✔
461
    }
1✔
462
}
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