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

trane-project / trane / 4300012342

pending completion
4300012342

Pull #193

github

GitHub
Merge 4e068284d into c270a1c18
Pull Request #193: Specify instrument in transcription exercises

21 of 21 new or added lines in 1 file covered. (100.0%)

5403 of 5403 relevant lines covered (100.0%)

93878.67 hits per line

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

100.0
/src/course_builder/knowledge_base_builder.rs
1
//! Contains utilities to make it easier to build knowledge base courses.
2
//!
3
//! The knowledge base course format is a plain-text format that is intended to be easy to edit by
4
//! hand. This module contains utilities to make it easier to generate these files, specially for
5
//! testing purposes.
6

7
use anyhow::{ensure, Context, Result};
8
use serde::{Deserialize, Serialize};
9
use std::{
10
    collections::{BTreeMap, HashSet},
11
    fs::{self, create_dir_all, File},
12
    io::Write,
13
    path::Path,
14
};
15
use ustr::Ustr;
16

17
use crate::{
18
    course_builder::AssetBuilder,
19
    course_library::COURSE_MANIFEST_FILENAME,
20
    data::{course_generator::knowledge_base::*, CourseManifest},
21
};
22

23
/// A builder to generate a knowledge base exercise and associated assets.
24
pub struct ExerciseBuilder {
25
    /// The knowledge base exercise to build.
26
    pub exercise: KnowledgeBaseExercise,
27

28
    /// The assets associated with this exercise, which include the front and back of the flashcard.
29
    pub asset_builders: Vec<AssetBuilder>,
30
}
31

32
impl ExerciseBuilder {
33
    /// Writes the files needed for this exercise to the given directory.
34
    pub fn build(&self, lesson_directory: &Path) -> Result<()> {
105✔
35
        // Build all the assets.
36
        for builder in &self.asset_builders {
273✔
37
            builder.build(lesson_directory)?;
168✔
38
        }
39

40
        // Write the exercise properties to the corresponding file.
41
        if let Some(name) = &self.exercise.name {
105✔
42
            let name_json = serde_json::to_string_pretty(name)?;
1✔
43
            let name_path = lesson_directory.join(format!(
1✔
44
                "{}{}",
45
                self.exercise.short_id, EXERCISE_NAME_SUFFIX
46
            ));
47
            let mut name_file = File::create(name_path)?;
1✔
48
            name_file.write_all(name_json.as_bytes())?;
1✔
49
        }
1✔
50
        if let Some(description) = &self.exercise.description {
105✔
51
            let description_json = serde_json::to_string_pretty(description)?;
1✔
52
            let description_path = lesson_directory.join(format!(
1✔
53
                "{}{}",
54
                self.exercise.short_id, EXERCISE_DESCRIPTION_SUFFIX
55
            ));
56
            let mut description_file = File::create(description_path)?;
1✔
57
            description_file.write_all(description_json.as_bytes())?;
1✔
58
        }
1✔
59
        if let Some(exercise_type) = &self.exercise.exercise_type {
105✔
60
            let exercise_type_json = serde_json::to_string_pretty(exercise_type)?;
1✔
61
            let exercise_type_path = lesson_directory.join(format!(
1✔
62
                "{}{}",
63
                self.exercise.short_id, EXERCISE_TYPE_SUFFIX
64
            ));
65
            let mut exercise_type_file = File::create(exercise_type_path)?;
1✔
66
            exercise_type_file.write_all(exercise_type_json.as_bytes())?;
1✔
67
        }
1✔
68
        Ok(())
105✔
69
    }
105✔
70
}
71

72
/// A builder to generate a knowledge base lesson and associated assets.
73
pub struct LessonBuilder {
74
    /// The knowledge base lesson to build.
75
    pub lesson: KnowledgeBaseLesson,
76

77
    /// The exercise builders for this lesson.
78
    pub exercises: Vec<ExerciseBuilder>,
79

80
    /// The assets associated with this lesson, which include the lesson instructions and materials.
81
    pub asset_builders: Vec<AssetBuilder>,
82
}
83

84
impl LessonBuilder {
85
    /// Writes the files needed for this lesson to the given directory.
86
    pub fn build(&self, lesson_directory: &Path) -> Result<()> {
23✔
87
        // Create the lesson directory.
88
        create_dir_all(lesson_directory)?;
23✔
89

90
        // Build all the assets.
91
        for builder in &self.asset_builders {
28✔
92
            builder.build(lesson_directory)?;
5✔
93
        }
94

95
        // Build all the exercises.
96
        for builder in &self.exercises {
128✔
97
            builder.build(lesson_directory)?;
105✔
98
        }
99

100
        // Write the lesson properties to the corresponding file.
101
        if let Some(name) = &self.lesson.name {
23✔
102
            let name_json = serde_json::to_string_pretty(name)?;
1✔
103
            let name_path = lesson_directory.join(LESSON_NAME_FILE);
1✔
104
            let mut name_file = File::create(name_path)?;
1✔
105
            name_file.write_all(name_json.as_bytes())?;
1✔
106
        }
1✔
107
        if let Some(description) = &self.lesson.description {
23✔
108
            let description_json = serde_json::to_string_pretty(description)?;
1✔
109
            let description_path = lesson_directory.join(LESSON_DESCRIPTION_FILE);
1✔
110
            let mut description_file = File::create(description_path)?;
1✔
111
            description_file.write_all(description_json.as_bytes())?;
1✔
112
        }
1✔
113
        if let Some(dependencies) = &self.lesson.dependencies {
23✔
114
            let dependencies_json = serde_json::to_string_pretty(dependencies)?;
22✔
115
            let dependencies_path = lesson_directory.join(LESSON_DEPENDENCIES_FILE);
22✔
116
            let mut dependencies_file = File::create(dependencies_path)?;
22✔
117
            dependencies_file.write_all(dependencies_json.as_bytes())?;
22✔
118
        }
22✔
119
        if let Some(metadata) = &self.lesson.metadata {
23✔
120
            let metadata_json = serde_json::to_string_pretty(metadata)?;
2✔
121
            let metadata_path = lesson_directory.join(LESSON_METADATA_FILE);
2✔
122
            let mut metadata_file = File::create(metadata_path)?;
2✔
123
            metadata_file.write_all(metadata_json.as_bytes())?;
2✔
124
        }
2✔
125
        Ok(())
23✔
126
    }
23✔
127
}
128

129
/// A builder to generate a knowledge base course and associated assets.
130
pub struct CourseBuilder {
131
    /// Base name of the directory on which to store this course.
132
    pub directory_name: String,
133

134
    /// The builders for the lessons in this course.
135
    pub lessons: Vec<LessonBuilder>,
136

137
    /// The assets associated with this course.
138
    pub assets: Vec<AssetBuilder>,
139

140
    /// The manifest for this course.
141
    pub manifest: CourseManifest,
142
}
143

144
impl CourseBuilder {
145
    /// Writes the files needed for this course to the given directory.
146
    pub fn build(&self, parent_directory: &Path) -> Result<()> {
3✔
147
        // Verify that the directory doesn't already exist and create it.
148
        let course_directory = parent_directory.join(&self.directory_name);
3✔
149
        ensure!(
3✔
150
            !course_directory.is_dir(),
3✔
151
            "course directory {} already exists",
152
            course_directory.display(), // grcov-excl-line
153
        );
154
        create_dir_all(&course_directory)?;
3✔
155

156
        // Write all the assets.
157
        for builder in &self.assets {
5✔
158
            builder.build(&course_directory)?;
2✔
159
        }
160

161
        // For each lesson in the course, create a directory with the name
162
        // `<LESSON_SHORT_ID>.lesson` and build the lesson in that directory.
163
        for builder in &self.lessons {
24✔
164
            let lesson_directory =
165
                course_directory.join(format!("{}{}", builder.lesson.short_id, LESSON_SUFFIX));
21✔
166
            fs::create_dir_all(&lesson_directory)?;
21✔
167
            builder.build(&lesson_directory)?;
21✔
168
        }
21✔
169

170
        // Write the manifest to disk.
171
        let manifest_json = serde_json::to_string_pretty(&self.manifest)? + "\n";
3✔
172
        let manifest_path = course_directory.join("course_manifest.json");
3✔
173
        let mut manifest_file = File::create(manifest_path)?;
3✔
174
        manifest_file.write_all(manifest_json.as_bytes())?;
3✔
175
        Ok(())
3✔
176
    }
3✔
177
}
178

179
/// Represents a simple knowledge base exercise which only specifies the short ID of the exercise,
180
/// and the front and (optional) back of the card, which in a lot of cases are enough to deliver full
181
/// functionality of Trane. It is meant to help course authors write simple knowledge base courses by
182
/// writing a simple configuration to a single JSON file.
183
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184
pub struct SimpleKnowledgeBaseExercise {
185
    /// The short ID of the exercise.
186
    pub short_id: String,
8✔
187

188
    /// The content of the front of the card.
189
    pub front: Vec<String>,
8✔
190

191
    /// The optional content of the back of the card. If the list is empty, no file will be created.
192
    pub back: Vec<String>,
8✔
193
}
194

195
impl SimpleKnowledgeBaseExercise {
196
    /// Generates the exercise builder for this simple knowledge base exercise.
197
    fn generate_exercise_builder(
5✔
198
        &self,
199
        short_lesson_id: Ustr,
200
        course_id: Ustr,
201
    ) -> Result<ExerciseBuilder> {
202
        // Ensure that the short ID is not empty.
203
        ensure!(!self.short_id.is_empty(), "short ID cannot be empty");
5✔
204

205
        // Generate the asset builders for the front and back of the card.
206
        let front_file = format!("{}{}", self.short_id, EXERCISE_FRONT_SUFFIX);
4✔
207
        let back_file = if !self.back.is_empty() {
4✔
208
            Some(format!("{}{}", self.short_id, EXERCISE_BACK_SUFFIX))
2✔
209
        } else {
210
            None
2✔
211
        };
212

213
        let mut asset_builders = vec![AssetBuilder {
8✔
214
            file_name: front_file.clone(),
4✔
215
            contents: self.front.join("\n"),
4✔
216
        }];
217
        if !self.back.is_empty() {
4✔
218
            asset_builders.push(AssetBuilder {
2✔
219
                file_name: back_file.clone().unwrap(),
2✔
220
                contents: self.back.join("\n"),
2✔
221
            })
222
        }
223

224
        // Generate the exercise builder.
225
        Ok(ExerciseBuilder {
4✔
226
            exercise: KnowledgeBaseExercise {
4✔
227
                short_id: self.short_id.to_string(),
4✔
228
                short_lesson_id,
229
                course_id,
230
                front_file,
4✔
231
                back_file,
4✔
232
                name: None,
4✔
233
                description: None,
4✔
234
                exercise_type: None,
235
            },
236
            asset_builders,
4✔
237
        })
238
    }
5✔
239
}
240

241
/// Represents a simple knowledge base lesson which only specifies the short ID of the lesson, the
242
/// dependencies of the lesson, and a list of simple exercises. The instructions, material, and
243
/// metadata can be optionally specified as well. In a lot of cases, this is enough to deliver the
244
/// full functionality of Trane. It is meant to help course authors write simple knowledge base
245
/// courses by writing a simple configuration to a single JSON file.
246
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
247
pub struct SimpleKnowledgeBaseLesson {
248
    /// The short ID of the lesson.
249
    pub short_id: Ustr,
4✔
250

251
    /// The dependencies of the lesson.
252
    pub dependencies: Vec<Ustr>,
4✔
253

254
    /// The simple exercises in the lesson.
255
    pub exercises: Vec<SimpleKnowledgeBaseExercise>,
4✔
256

257
    /// The optional metadata for the lesson.
258
    pub metadata: Option<BTreeMap<String, Vec<String>>>,
4✔
259

260
    /// A list of additional files to write in the lesson directory.
261
    pub additional_files: Vec<AssetBuilder>,
4✔
262
}
263

264
impl SimpleKnowledgeBaseLesson {
265
    /// Generates the lesson builder from this simple lesson.
266
    fn generate_lesson_builder(&self, course_id: Ustr) -> Result<LessonBuilder> {
5✔
267
        // Ensure that the lesson short ID is not empty and that the exercise short IDs are unique.
268
        ensure!(
5✔
269
            !self.short_id.is_empty(),
5✔
270
            "short ID of lesson cannot be empty"
271
        );
272
        let mut short_ids = HashSet::new();
4✔
273
        for exercise in &self.exercises {
10✔
274
            ensure!(
7✔
275
                !short_ids.contains(&exercise.short_id),
7✔
276
                "short ID {} of exercise is not unique",
277
                exercise.short_id
278
            );
279
            short_ids.insert(&exercise.short_id);
6✔
280
        }
281

282
        // Generate the exercise builders.
283
        let exercises = self
6✔
284
            .exercises
285
            .iter()
286
            .map(|exercise| exercise.generate_exercise_builder(self.short_id, course_id))
8✔
287
            .collect::<Result<Vec<_>>>()?;
1✔
288

289
        // Generate the lesson builder.
290
        let dependencies = if self.dependencies.is_empty() {
2✔
291
            None
1✔
292
        } else {
293
            Some(self.dependencies.clone())
1✔
294
        };
295
        let has_instructions = self
2✔
296
            .additional_files
297
            .iter()
298
            .any(|asset| asset.file_name == LESSON_INSTRUCTIONS_FILE);
2✔
299
        let has_material = self
2✔
300
            .additional_files
301
            .iter()
302
            .any(|asset| asset.file_name == LESSON_MATERIAL_FILE);
3✔
303
        let lesson_builder = LessonBuilder {
2✔
304
            lesson: KnowledgeBaseLesson {
2✔
305
                short_id: self.short_id,
2✔
306
                course_id,
2✔
307
                dependencies,
2✔
308
                name: None,
2✔
309
                description: None,
2✔
310
                metadata: self.metadata.clone(),
2✔
311
                has_instructions,
312
                has_material,
313
            },
314
            exercises,
2✔
315
            asset_builders: self.additional_files.clone(),
2✔
316
        };
317
        Ok(lesson_builder)
2✔
318
    }
5✔
319
}
320

321
/// Represents a simple knowledge base course which only specifies the course manifest and a list of
322
/// simple lessons. It is meant to help course authors write simple knowledge base courses by
323
/// writing a simple configuration to a single JSON file.
324
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
325
pub struct SimpleKnowledgeBaseCourse {
326
    /// The manifest for this course.
327
    pub manifest: CourseManifest,
2✔
328

329
    /// The simple lessons in this course.
330
    pub lessons: Vec<SimpleKnowledgeBaseLesson>,
2✔
331
}
332

333
impl SimpleKnowledgeBaseCourse {
334
    /// Writes the course manifests and the lesson directories with the assets and exercises to the
335
    /// given directory.
336
    pub fn build(&self, root_directory: &Path) -> Result<()> {
5✔
337
        // Ensure that the lesson short IDs are unique.
338
        let mut short_ids = HashSet::new();
5✔
339
        for lesson in &self.lessons {
11✔
340
            ensure!(
7✔
341
                !short_ids.contains(&lesson.short_id),
7✔
342
                "short ID {} of lesson is not unique",
343
                lesson.short_id
344
            );
345
            short_ids.insert(&lesson.short_id);
6✔
346
        }
347

348
        // Generate the lesson builders.
349
        let lesson_builders = self
8✔
350
            .lessons
351
            .iter()
352
            .map(|lesson| lesson.generate_lesson_builder(self.manifest.id))
9✔
353
            .collect::<Result<Vec<_>>>()?;
3✔
354

355
        // Build the lessons in the course.
356
        for lesson_builder in lesson_builders {
3✔
357
            let lesson_directory = root_directory.join(format!(
2✔
358
                "{}{}",
359
                lesson_builder.lesson.short_id, LESSON_SUFFIX
360
            ));
361

362
            // Remove the lesson directories if they already exist.
363
            if lesson_directory.exists() {
3✔
364
                fs::remove_dir_all(&lesson_directory).with_context(|| {
1✔
365
                    // grcov-excl-start
366
                    format!(
367
                        "failed to remove existing lesson directory at {}",
368
                        lesson_directory.display()
369
                    )
370
                    // grcov-excl-stop
371
                })?; // grcov-excl-line
372
            }
373

374
            lesson_builder.build(&lesson_directory)?;
2✔
375
        }
2✔
376

377
        // Write the course manifest.
378
        let manifest_path = root_directory.join(COURSE_MANIFEST_FILENAME);
1✔
379
        let mut manifest_file = fs::File::create(&manifest_path).with_context(|| {
1✔
380
            // grcov-excl-start
381
            format!(
382
                "failed to create course manifest file at {}",
383
                manifest_path.display()
384
            )
385
            // grcov-excl-stop
386
        })?; // grcov-excl-line
387
        manifest_file
2✔
388
            .write_all(serde_json::to_string_pretty(&self.manifest)?.as_bytes())
1✔
389
            .with_context(|| {
1✔
390
                // grcov-excl-start
391
                format!(
392
                    "failed to write course manifest file at {}",
393
                    manifest_path.display()
394
                )
395
                // grcov-excl-stop
396
            }) // grcov-excl-line
397
    }
5✔
398
}
399

400
#[cfg(test)]
401
mod test {
402
    use std::collections::BTreeMap;
403

404
    use anyhow::Result;
405

406
    use crate::{
407
        course_builder::knowledge_base_builder::*,
408
        data::{course_generator::knowledge_base::KnowledgeBaseFile, ExerciseType},
409
    };
410

411
    /// Creates a test lesson builder.
412
    fn test_lesson_builder() -> LessonBuilder {
1✔
413
        let exercise_builder = ExerciseBuilder {
1✔
414
            exercise: KnowledgeBaseExercise {
1✔
415
                short_id: "ex1".to_string(),
1✔
416
                short_lesson_id: "lesson1".into(),
1✔
417
                course_id: "course1".into(),
1✔
418
                front_file: "ex1.front.md".to_string(),
1✔
419
                back_file: Some("ex1.back.md".to_string()),
1✔
420
                name: Some("Exercise 1".to_string()),
1✔
421
                description: Some("Exercise 1 description".to_string()),
1✔
422
                exercise_type: Some(ExerciseType::Procedural),
423
            },
424
            asset_builders: vec![
2✔
425
                AssetBuilder {
1✔
426
                    file_name: "ex1.front.md".to_string(),
1✔
427
                    contents: "Exercise 1 front".to_string(),
1✔
428
                },
429
                AssetBuilder {
1✔
430
                    file_name: "ex1.back.md".to_string(),
1✔
431
                    contents: "Exercise 1 back".to_string(),
1✔
432
                },
433
            ],
434
        };
435
        LessonBuilder {
1✔
436
            lesson: KnowledgeBaseLesson {
1✔
437
                short_id: "lesson1".into(),
1✔
438
                course_id: "course1".into(),
1✔
439
                name: Some("Lesson 1".to_string()),
1✔
440
                description: Some("Lesson 1 description".to_string()),
1✔
441
                dependencies: Some(vec!["lesson2".into()]),
1✔
442
                metadata: Some(BTreeMap::from([(
1✔
443
                    "key".to_string(),
1✔
444
                    vec!["value".to_string()],
1✔
445
                )])),
446
                has_instructions: true,
447
                has_material: true,
448
            },
449
            exercises: vec![exercise_builder],
1✔
450
            asset_builders: vec![
2✔
451
                AssetBuilder {
1✔
452
                    file_name: LESSON_INSTRUCTIONS_FILE.to_string(),
1✔
453
                    contents: "Instructions".to_string(),
1✔
454
                },
455
                AssetBuilder {
1✔
456
                    file_name: LESSON_MATERIAL_FILE.to_string(),
1✔
457
                    contents: "Material".to_string(),
1✔
458
                },
459
            ],
460
        }
461
    }
1✔
462

463
    /// Verifies that the course builder writes the correct files to disk.
464
    #[test]
465
    fn course_builder() -> Result<()> {
2✔
466
        let temp_dir = tempfile::tempdir()?;
1✔
467
        let course_builder = CourseBuilder {
1✔
468
            directory_name: "course1".into(),
1✔
469
            manifest: CourseManifest {
1✔
470
                id: "course1".into(),
1✔
471
                name: "Course 1".into(),
1✔
472
                dependencies: vec![],
1✔
473
                description: None,
1✔
474
                authors: None,
1✔
475
                metadata: None,
1✔
476
                course_material: None,
1✔
477
                course_instructions: None,
1✔
478
                generator_config: None,
1✔
479
            },
480
            lessons: vec![test_lesson_builder()],
1✔
481
            assets: vec![
2✔
482
                AssetBuilder {
1✔
483
                    file_name: "course_instructions.md".to_string(),
1✔
484
                    contents: "Course Instructions".to_string(),
1✔
485
                },
486
                AssetBuilder {
1✔
487
                    file_name: "course_material.md".to_string(),
1✔
488
                    contents: "Course Material".to_string(),
1✔
489
                },
490
            ],
491
        };
492

493
        course_builder.build(temp_dir.path())?;
1✔
494

495
        // Verify that the exercise was built correctly.
496
        let course_dir = temp_dir.path().join("course1");
1✔
497
        let lesson_dir = course_dir.join("lesson1.lesson");
1✔
498
        assert!(lesson_dir.exists());
1✔
499
        let front_file = lesson_dir.join("ex1.front.md");
1✔
500
        assert!(front_file.exists());
1✔
501
        assert_eq!(fs::read_to_string(front_file)?, "Exercise 1 front");
1✔
502
        let back_file = lesson_dir.join("ex1.back.md");
1✔
503
        assert!(back_file.exists());
1✔
504
        assert_eq!(fs::read_to_string(back_file)?, "Exercise 1 back");
1✔
505
        let name_file = lesson_dir.join("ex1.name.json");
1✔
506
        assert!(name_file.exists());
1✔
507
        assert_eq!(KnowledgeBaseFile::open::<String>(&name_file)?, "Exercise 1",);
1✔
508
        let description_file = lesson_dir.join("ex1.description.json");
1✔
509
        assert!(description_file.exists());
1✔
510
        assert_eq!(
1✔
511
            KnowledgeBaseFile::open::<String>(&description_file)?,
1✔
512
            "Exercise 1 description",
513
        );
514
        let type_file = lesson_dir.join("ex1.type.json");
1✔
515
        assert!(type_file.exists());
1✔
516
        assert_eq!(
1✔
517
            KnowledgeBaseFile::open::<ExerciseType>(&type_file)?,
1✔
518
            ExerciseType::Procedural,
519
        );
520

521
        // Verify that the lesson was built correctly.
522
        let name_file = lesson_dir.join(LESSON_NAME_FILE);
1✔
523
        assert!(name_file.exists());
1✔
524
        assert_eq!(KnowledgeBaseFile::open::<String>(&name_file)?, "Lesson 1",);
1✔
525
        let description_file = lesson_dir.join(LESSON_DESCRIPTION_FILE);
1✔
526
        assert!(description_file.exists());
1✔
527
        assert_eq!(
1✔
528
            KnowledgeBaseFile::open::<String>(&description_file)?,
1✔
529
            "Lesson 1 description",
530
        );
531
        let dependencies_file = lesson_dir.join(LESSON_DEPENDENCIES_FILE);
1✔
532
        assert!(lesson_dir.join(LESSON_DEPENDENCIES_FILE).exists());
1✔
533
        assert_eq!(
1✔
534
            KnowledgeBaseFile::open::<Vec<String>>(&dependencies_file)?,
1✔
535
            vec!["lesson2".to_string()],
1✔
536
        );
537
        let metadata_file = lesson_dir.join(LESSON_METADATA_FILE);
1✔
538
        assert!(metadata_file.exists());
1✔
539
        assert_eq!(
1✔
540
            KnowledgeBaseFile::open::<BTreeMap<String, Vec<String>>>(&metadata_file)?,
1✔
541
            BTreeMap::from([("key".to_string(), vec!["value".to_string()])]),
1✔
542
        );
543
        let instructions_file = lesson_dir.join(LESSON_INSTRUCTIONS_FILE);
1✔
544
        assert!(instructions_file.exists());
1✔
545
        assert_eq!(fs::read_to_string(instructions_file)?, "Instructions",);
1✔
546
        let material_file = lesson_dir.join(LESSON_MATERIAL_FILE);
1✔
547
        assert!(material_file.exists());
1✔
548
        assert_eq!(fs::read_to_string(material_file)?, "Material",);
1✔
549

550
        // Verify that the course was built correctly.
551
        assert!(course_dir.join("course_manifest.json").exists());
1✔
552
        assert_eq!(
1✔
553
            KnowledgeBaseFile::open::<CourseManifest>(&course_dir.join("course_manifest.json"))
1✔
554
                .unwrap(),
555
            course_builder.manifest,
556
        );
557
        assert!(course_dir.join("course_instructions.md").exists());
1✔
558
        assert_eq!(
1✔
559
            fs::read_to_string(course_dir.join("course_instructions.md"))?,
1✔
560
            "Course Instructions",
561
        );
562
        assert!(course_dir.join("course_material.md").exists());
1✔
563
        assert_eq!(
1✔
564
            fs::read_to_string(course_dir.join("course_material.md"))?,
1✔
565
            "Course Material",
566
        );
567

568
        Ok(())
1✔
569
    }
2✔
570

571
    /// Verifies that the simple course builder writes the correct files to disk.
572
    #[test]
573
    fn build_simple_course() -> Result<()> {
2✔
574
        // Create a simple course. The first lesson sets up the minimum required fields for a
575
        // lesson, and the second lesson sets up all optional fields.
576
        let simple_course = SimpleKnowledgeBaseCourse {
1✔
577
            manifest: CourseManifest {
1✔
578
                id: "course1".into(),
1✔
579
                name: "Course 1".into(),
1✔
580
                dependencies: vec![],
1✔
581
                description: None,
1✔
582
                authors: None,
1✔
583
                metadata: None,
1✔
584
                course_material: None,
1✔
585
                course_instructions: None,
1✔
586
                generator_config: None,
1✔
587
            },
588
            lessons: vec![
2✔
589
                SimpleKnowledgeBaseLesson {
1✔
590
                    short_id: "1".into(),
1✔
591
                    dependencies: vec![],
1✔
592
                    exercises: vec![
2✔
593
                        SimpleKnowledgeBaseExercise {
1✔
594
                            short_id: "1".into(),
1✔
595
                            front: vec!["Lesson 1, Exercise 1 front".into()],
1✔
596
                            back: vec![],
1✔
597
                        },
598
                        SimpleKnowledgeBaseExercise {
1✔
599
                            short_id: "2".into(),
1✔
600
                            front: vec!["Lesson 1, Exercise 2 front".into()],
1✔
601
                            back: vec![],
1✔
602
                        },
603
                    ],
604
                    metadata: None,
1✔
605
                    additional_files: vec![],
1✔
606
                },
607
                SimpleKnowledgeBaseLesson {
1✔
608
                    short_id: "2".into(),
1✔
609
                    dependencies: vec!["1".into()],
1✔
610
                    exercises: vec![
2✔
611
                        SimpleKnowledgeBaseExercise {
1✔
612
                            short_id: "1".into(),
1✔
613
                            front: vec!["Lesson 2, Exercise 1 front".into()],
1✔
614
                            back: vec!["Lesson 2, Exercise 1 back".into()],
1✔
615
                        },
616
                        SimpleKnowledgeBaseExercise {
1✔
617
                            short_id: "2".into(),
1✔
618
                            front: vec!["Lesson 2, Exercise 2 front".into()],
1✔
619
                            back: vec!["Lesson 2, Exercise 2 back".into()],
1✔
620
                        },
621
                    ],
622
                    metadata: Some(BTreeMap::from([(
1✔
623
                        "key".to_string(),
1✔
624
                        vec!["value".to_string()],
1✔
625
                    )])),
626
                    additional_files: vec![
2✔
627
                        AssetBuilder {
1✔
628
                            file_name: "dummy.md".into(),
1✔
629
                            contents: "I'm a dummy file".into(),
1✔
630
                        },
631
                        AssetBuilder {
1✔
632
                            file_name: LESSON_INSTRUCTIONS_FILE.into(),
1✔
633
                            contents: "Lesson 2 instructions".into(),
1✔
634
                        },
635
                        AssetBuilder {
1✔
636
                            file_name: LESSON_MATERIAL_FILE.into(),
1✔
637
                            contents: "Lesson 2 material".into(),
1✔
638
                        },
639
                    ],
640
                },
641
            ],
642
        };
643

644
        // Create a temp directory and one of the lesson directories with some content to ensure
645
        // that is deleted. Then build the course and verify the contents of the output directory.
646
        let temp_dir = tempfile::tempdir()?;
1✔
647
        let dummy_dir = temp_dir.path().join("1.lesson").join("dummy");
1✔
648
        fs::create_dir_all(&dummy_dir)?;
1✔
649
        assert!(dummy_dir.exists());
1✔
650
        simple_course.build(&temp_dir.path())?;
1✔
651
        assert!(!dummy_dir.exists());
1✔
652

653
        // Verify that the first lesson was built correctly.
654
        let lesson_dir = temp_dir.path().join("1.lesson");
1✔
655
        assert!(lesson_dir.exists());
1✔
656
        let front_file = lesson_dir.join("1.front.md");
1✔
657
        assert!(front_file.exists());
1✔
658
        assert_eq!(
1✔
659
            fs::read_to_string(&front_file)?,
1✔
660
            "Lesson 1, Exercise 1 front"
661
        );
662
        let front_file = lesson_dir.join("2.front.md");
1✔
663
        assert!(front_file.exists());
1✔
664
        assert_eq!(
1✔
665
            fs::read_to_string(&front_file)?,
1✔
666
            "Lesson 1, Exercise 2 front"
667
        );
668
        let dependencies_file = lesson_dir.join(LESSON_DEPENDENCIES_FILE);
1✔
669
        assert!(!dependencies_file.exists());
1✔
670
        let instructions_file = lesson_dir.join(LESSON_INSTRUCTIONS_FILE);
1✔
671
        assert!(!instructions_file.exists());
1✔
672
        let material_file = lesson_dir.join(LESSON_MATERIAL_FILE);
1✔
673
        assert!(!material_file.exists());
1✔
674

675
        // Verify that the second lesson was built correctly.
676
        let lesson_dir = temp_dir.path().join("2.lesson");
1✔
677
        assert!(lesson_dir.exists());
1✔
678
        let front_file = lesson_dir.join("1.front.md");
1✔
679
        assert!(front_file.exists());
1✔
680
        assert_eq!(
1✔
681
            fs::read_to_string(&front_file)?,
1✔
682
            "Lesson 2, Exercise 1 front"
683
        );
684
        let back_file = lesson_dir.join("1.back.md");
1✔
685
        assert!(back_file.exists());
1✔
686
        assert_eq!(fs::read_to_string(&back_file)?, "Lesson 2, Exercise 1 back");
1✔
687
        let front_file = lesson_dir.join("2.front.md");
1✔
688
        assert!(front_file.exists());
1✔
689
        assert_eq!(
1✔
690
            fs::read_to_string(&front_file)?,
1✔
691
            "Lesson 2, Exercise 2 front"
692
        );
693
        let back_file = lesson_dir.join("2.back.md");
1✔
694
        assert!(back_file.exists());
1✔
695
        assert_eq!(fs::read_to_string(&back_file)?, "Lesson 2, Exercise 2 back");
1✔
696
        let dependencies_file = lesson_dir.join(LESSON_DEPENDENCIES_FILE);
1✔
697
        assert!(dependencies_file.exists());
1✔
698
        assert_eq!(
1✔
699
            KnowledgeBaseFile::open::<Vec<String>>(&dependencies_file)?,
1✔
700
            vec!["1".to_string()]
1✔
701
        );
702
        let instructions_file = lesson_dir.join(LESSON_INSTRUCTIONS_FILE);
1✔
703
        assert!(instructions_file.exists());
1✔
704
        assert_eq!(
1✔
705
            fs::read_to_string(&instructions_file)?,
1✔
706
            "Lesson 2 instructions"
707
        );
708
        let material_file = lesson_dir.join(LESSON_MATERIAL_FILE);
1✔
709
        assert!(material_file.exists());
1✔
710
        assert_eq!(fs::read_to_string(&material_file)?, "Lesson 2 material");
1✔
711
        let metadata_file = lesson_dir.join(LESSON_METADATA_FILE);
1✔
712
        assert!(metadata_file.exists());
1✔
713
        assert_eq!(
1✔
714
            KnowledgeBaseFile::open::<BTreeMap<String, Vec<String>>>(&metadata_file)?,
1✔
715
            BTreeMap::from([("key".to_string(), vec!["value".to_string()])])
1✔
716
        );
717
        let dummy_file = lesson_dir.join("dummy.md");
1✔
718
        assert!(dummy_file.exists());
1✔
719
        assert_eq!(fs::read_to_string(&dummy_file)?, "I'm a dummy file");
1✔
720

721
        // Finally, clone the simple knowledge course to satisfy the code coverage check.
722
        assert_eq!(simple_course.clone(), simple_course);
1✔
723
        Ok(())
1✔
724
    }
2✔
725

726
    // Verifies that the simple knowledge course checks for duplicate lesson IDs.
727
    #[test]
728
    fn duplicate_short_lesson_ids() -> Result<()> {
2✔
729
        // Build a simple course with duplicate lesson IDs.
730
        let simple_course = SimpleKnowledgeBaseCourse {
1✔
731
            manifest: CourseManifest {
1✔
732
                id: "course1".into(),
1✔
733
                name: "Course 1".into(),
1✔
734
                dependencies: vec![],
1✔
735
                description: None,
1✔
736
                authors: None,
1✔
737
                metadata: None,
1✔
738
                course_material: None,
1✔
739
                course_instructions: None,
1✔
740
                generator_config: None,
1✔
741
            },
742
            lessons: vec![
2✔
743
                SimpleKnowledgeBaseLesson {
1✔
744
                    short_id: "1".into(),
1✔
745
                    dependencies: vec![],
1✔
746
                    exercises: vec![SimpleKnowledgeBaseExercise {
2✔
747
                        short_id: "1".into(),
1✔
748
                        front: vec!["Lesson 1, Exercise 1 front".into()],
1✔
749
                        back: vec![],
1✔
750
                    }],
751
                    metadata: None,
1✔
752
                    additional_files: vec![],
1✔
753
                },
754
                SimpleKnowledgeBaseLesson {
1✔
755
                    short_id: "1".into(),
1✔
756
                    dependencies: vec![],
1✔
757
                    exercises: vec![SimpleKnowledgeBaseExercise {
2✔
758
                        short_id: "1".into(),
1✔
759
                        front: vec!["Lesson 2, Exercise 1 front".into()],
1✔
760
                        back: vec![],
1✔
761
                    }],
762
                    metadata: None,
1✔
763
                    additional_files: vec![],
1✔
764
                },
765
            ],
766
        };
767

768
        // Verify that the course builder fails.
769
        let temp_dir = tempfile::tempdir()?;
1✔
770
        assert!(simple_course.build(&temp_dir.path()).is_err());
1✔
771
        Ok(())
1✔
772
    }
2✔
773

774
    // Verifies that the simple knowledge course checks for duplicate exercise IDs.
775
    #[test]
776
    fn duplicate_short_exercise_ids() -> Result<()> {
2✔
777
        // Build a simple course with duplicate exercise IDs.
778
        let simple_course = SimpleKnowledgeBaseCourse {
1✔
779
            manifest: CourseManifest {
1✔
780
                id: "course1".into(),
1✔
781
                name: "Course 1".into(),
1✔
782
                dependencies: vec![],
1✔
783
                description: None,
1✔
784
                authors: None,
1✔
785
                metadata: None,
1✔
786
                course_material: None,
1✔
787
                course_instructions: None,
1✔
788
                generator_config: None,
1✔
789
            },
790
            lessons: vec![SimpleKnowledgeBaseLesson {
2✔
791
                short_id: "1".into(),
1✔
792
                dependencies: vec![],
1✔
793
                exercises: vec![
2✔
794
                    SimpleKnowledgeBaseExercise {
1✔
795
                        short_id: "1".into(),
1✔
796
                        front: vec!["Lesson 1, Exercise 1 front".into()],
1✔
797
                        back: vec![],
1✔
798
                    },
799
                    SimpleKnowledgeBaseExercise {
1✔
800
                        short_id: "1".into(),
1✔
801
                        front: vec!["Lesson 1, Exercise 2 front".into()],
1✔
802
                        back: vec![],
1✔
803
                    },
804
                ],
805
                metadata: None,
1✔
806
                additional_files: vec![],
1✔
807
            }],
808
        };
809

810
        // Verify that the course builder fails.
811
        let temp_dir = tempfile::tempdir()?;
1✔
812
        assert!(simple_course.build(&temp_dir.path()).is_err());
1✔
813
        Ok(())
1✔
814
    }
2✔
815

816
    // Verifies that the simple knowledge course checks empty lesson IDs.
817
    #[test]
818
    fn empty_short_lesson_ids() -> Result<()> {
2✔
819
        // Build a simple course with empty lesson IDs.
820
        let simple_course = SimpleKnowledgeBaseCourse {
1✔
821
            manifest: CourseManifest {
1✔
822
                id: "course1".into(),
1✔
823
                name: "Course 1".into(),
1✔
824
                dependencies: vec![],
1✔
825
                description: None,
1✔
826
                authors: None,
1✔
827
                metadata: None,
1✔
828
                course_material: None,
1✔
829
                course_instructions: None,
1✔
830
                generator_config: None,
1✔
831
            },
832
            lessons: vec![SimpleKnowledgeBaseLesson {
2✔
833
                short_id: "".into(),
1✔
834
                dependencies: vec![],
1✔
835
                exercises: vec![SimpleKnowledgeBaseExercise {
2✔
836
                    short_id: "1".into(),
1✔
837
                    front: vec!["Lesson 1, Exercise 1 front".into()],
1✔
838
                    back: vec![],
1✔
839
                }],
840
                metadata: None,
1✔
841
                additional_files: vec![],
1✔
842
            }],
843
        };
844

845
        // Verify that the course builder fails.
846
        let temp_dir = tempfile::tempdir()?;
1✔
847
        assert!(simple_course.build(&temp_dir.path()).is_err());
1✔
848
        Ok(())
1✔
849
    }
2✔
850

851
    // Verifies that the simple knowledge course checks empty exercise IDs.
852
    #[test]
853
    fn empty_short_exercise_ids() -> Result<()> {
2✔
854
        // Build a simple course with empty exercise IDs.
855
        let simple_course = SimpleKnowledgeBaseCourse {
1✔
856
            manifest: CourseManifest {
1✔
857
                id: "course1".into(),
1✔
858
                name: "Course 1".into(),
1✔
859
                dependencies: vec![],
1✔
860
                description: None,
1✔
861
                authors: None,
1✔
862
                metadata: None,
1✔
863
                course_material: None,
1✔
864
                course_instructions: None,
1✔
865
                generator_config: None,
1✔
866
            },
867
            lessons: vec![SimpleKnowledgeBaseLesson {
2✔
868
                short_id: "1".into(),
1✔
869
                dependencies: vec![],
1✔
870
                exercises: vec![SimpleKnowledgeBaseExercise {
2✔
871
                    short_id: "".into(),
1✔
872
                    front: vec!["Lesson 1, Exercise 1 front".into()],
1✔
873
                    back: vec![],
1✔
874
                }],
875
                metadata: None,
1✔
876
                additional_files: vec![],
1✔
877
            }],
878
        };
879

880
        // Verify that the course builder fails.
881
        let temp_dir = tempfile::tempdir()?;
1✔
882
        assert!(simple_course.build(&temp_dir.path()).is_err());
1✔
883
        Ok(())
1✔
884
    }
2✔
885
}
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

© 2026 Coveralls, Inc