• 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.68
/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
                "{}{}",
1✔
45
                self.exercise.short_id, EXERCISE_NAME_SUFFIX
1✔
46
            ));
1✔
47
            let mut name_file = File::create(name_path)?;
1✔
48
            name_file.write_all(name_json.as_bytes())?;
1✔
49
        }
104✔
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
                "{}{}",
1✔
54
                self.exercise.short_id, EXERCISE_DESCRIPTION_SUFFIX
1✔
55
            ));
1✔
56
            let mut description_file = File::create(description_path)?;
1✔
57
            description_file.write_all(description_json.as_bytes())?;
1✔
58
        }
104✔
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
                "{}{}",
1✔
63
                self.exercise.short_id, EXERCISE_TYPE_SUFFIX
1✔
64
            ));
1✔
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
        }
104✔
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.
23✔
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
        }
22✔
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
        }
22✔
113
        if !self.lesson.dependencies.is_empty() {
23✔
114
            let dependencies_json = serde_json::to_string_pretty(&self.lesson.dependencies)?;
17✔
115
            let dependencies_path = lesson_directory.join(LESSON_DEPENDENCIES_FILE);
17✔
116
            let mut dependencies_file = File::create(dependencies_path)?;
17✔
117
            dependencies_file.write_all(dependencies_json.as_bytes())?;
17✔
118
        }
6✔
119
        if !self.lesson.superseded.is_empty() {
23✔
120
            let superseded_json = serde_json::to_string_pretty(&self.lesson.superseded)?;
2✔
121
            let superseded_path = lesson_directory.join(LESSON_SUPERSEDED_FILE);
2✔
122
            let mut superseded_file = File::create(superseded_path)?;
2✔
123
            superseded_file.write_all(superseded_json.as_bytes())?;
2✔
124
        }
21✔
125
        if let Some(metadata) = &self.lesson.metadata {
23✔
126
            let metadata_json = serde_json::to_string_pretty(metadata)?;
2✔
127
            let metadata_path = lesson_directory.join(LESSON_METADATA_FILE);
2✔
128
            let mut metadata_file = File::create(metadata_path)?;
2✔
129
            metadata_file.write_all(metadata_json.as_bytes())?;
2✔
130
        }
21✔
131
        Ok(())
23✔
132
    }
23✔
133
}
134

135
/// A builder to generate a knowledge base course and associated assets.
136
pub struct CourseBuilder {
137
    /// Base name of the directory on which to store this course.
138
    pub directory_name: String,
139

140
    /// The builders for the lessons in this course.
141
    pub lessons: Vec<LessonBuilder>,
142

143
    /// The assets associated with this course.
144
    pub assets: Vec<AssetBuilder>,
145

146
    /// The manifest for this course.
147
    pub manifest: CourseManifest,
148
}
149

150
impl CourseBuilder {
151
    /// Writes the files needed for this course to the given directory.
152
    pub fn build(&self, parent_directory: &Path) -> Result<()> {
3✔
153
        // Verify that the directory doesn't already exist and create it.
3✔
154
        let course_directory = parent_directory.join(&self.directory_name);
3✔
155
        ensure!(
3✔
156
            !course_directory.is_dir(),
3✔
UNCOV
157
            "course directory {} already exists",
×
NEW
158
            course_directory.display(),
×
159
        );
160
        create_dir_all(&course_directory)?;
3✔
161

162
        // Write all the assets.
163
        for builder in &self.assets {
5✔
164
            builder.build(&course_directory)?;
2✔
165
        }
166

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

176
        // Write the manifest to disk.
177
        let manifest_json = serde_json::to_string_pretty(&self.manifest)? + "\n";
3✔
178
        let manifest_path = course_directory.join("course_manifest.json");
3✔
179
        let mut manifest_file = File::create(manifest_path)?;
3✔
180
        manifest_file.write_all(manifest_json.as_bytes())?;
3✔
181
        Ok(())
3✔
182
    }
3✔
183
}
184

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

194
    /// The content of the front of the card.
195
    pub front: Vec<String>,
196

197
    /// The optional content of the back of the card. If the list is empty, no file will be created.
198
    #[serde(default)]
199
    #[serde(skip_serializing_if = "Vec::is_empty")]
200
    pub back: Vec<String>,
201
}
202

203
impl SimpleKnowledgeBaseExercise {
204
    /// Generates the exercise builder for this simple knowledge base exercise.
205
    fn generate_exercise_builder(
5✔
206
        &self,
5✔
207
        short_lesson_id: Ustr,
5✔
208
        course_id: Ustr,
5✔
209
    ) -> Result<ExerciseBuilder> {
5✔
210
        // Ensure that the short ID is not empty.
5✔
211
        ensure!(!self.short_id.is_empty(), "short ID cannot be empty");
5✔
212

213
        // Generate the asset builders for the front and back of the card.
214
        let front_file = format!("{}{}", self.short_id, EXERCISE_FRONT_SUFFIX);
4✔
215
        let back_file = if self.back.is_empty() {
4✔
216
            None
2✔
217
        } else {
218
            Some(format!("{}{}", self.short_id, EXERCISE_BACK_SUFFIX))
2✔
219
        };
220

221
        let mut asset_builders = vec![AssetBuilder {
4✔
222
            file_name: front_file.clone(),
4✔
223
            contents: self.front.join("\n"),
4✔
224
        }];
4✔
225
        if !self.back.is_empty() {
4✔
226
            asset_builders.push(AssetBuilder {
2✔
227
                file_name: back_file.clone().unwrap(),
2✔
228
                contents: self.back.join("\n"),
2✔
229
            });
2✔
230
        }
2✔
231

232
        // Generate the exercise builder.
233
        Ok(ExerciseBuilder {
4✔
234
            exercise: KnowledgeBaseExercise {
4✔
235
                short_id: self.short_id.to_string(),
4✔
236
                short_lesson_id,
4✔
237
                course_id,
4✔
238
                front_file,
4✔
239
                back_file,
4✔
240
                name: None,
4✔
241
                description: None,
4✔
242
                exercise_type: None,
4✔
243
            },
4✔
244
            asset_builders,
4✔
245
        })
4✔
246
    }
5✔
247
}
248

249
//@<simple-knowledge-base-lesson
250
/// Represents a simple knowledge base lesson which only specifies the short ID of the lesson, the
251
/// dependencies of the lesson, and a list of simple exercises. The instructions, material, and
252
/// metadata can be optionally specified as well. In a lot of cases, this is enough to deliver the
253
/// full functionality of Trane. It is meant to help course authors write simple knowledge base
254
/// courses by writing a simple configuration to a single JSON file.
255
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
256
pub struct SimpleKnowledgeBaseLesson {
257
    /// The short ID of the lesson.
258
    pub short_id: Ustr,
259

260
    /// The dependencies of the lesson.
261
    #[serde(default)]
262
    pub dependencies: Vec<Ustr>,
263

264
    /// The courses or lessons that this lesson supersedes.
265
    #[serde(default)]
266
    pub superseded: Vec<Ustr>,
267

268
    /// The simple exercises in the lesson.
269
    #[serde(default)]
270
    pub exercises: Vec<SimpleKnowledgeBaseExercise>,
271

272
    /// The optional metadata for the lesson.
273
    #[serde(default)]
274
    #[serde(skip_serializing_if = "Option::is_none")]
275
    pub metadata: Option<BTreeMap<String, Vec<String>>>,
276

277
    /// A list of additional files to write in the lesson directory.
278
    #[serde(default)]
279
    #[serde(skip_serializing_if = "Vec::is_empty")]
280
    pub additional_files: Vec<AssetBuilder>,
281
}
282
//>@simple-knowledge-base-lesson
283

284
impl SimpleKnowledgeBaseLesson {
285
    /// Generates the lesson builder from this simple lesson.
286
    fn generate_lesson_builder(&self, course_id: Ustr) -> Result<LessonBuilder> {
5✔
287
        // Ensure that the lesson short ID is not empty and that the exercise short IDs are unique.
5✔
288
        ensure!(
5✔
289
            !self.short_id.is_empty(),
5✔
290
            "short ID of lesson cannot be empty"
1✔
291
        );
292
        let mut short_ids = HashSet::new();
4✔
293
        for exercise in &self.exercises {
10✔
294
            ensure!(
7✔
295
                !short_ids.contains(&exercise.short_id),
7✔
296
                "short ID {} of exercise is not unique",
1✔
297
                exercise.short_id
298
            );
299
            short_ids.insert(&exercise.short_id);
6✔
300
        }
301

302
        // Generate the exercise builders.
303
        let exercises = self
3✔
304
            .exercises
3✔
305
            .iter()
3✔
306
            .map(|exercise| exercise.generate_exercise_builder(self.short_id, course_id))
5✔
307
            .collect::<Result<Vec<_>>>()?;
3✔
308

309
        // Generate the lesson builder.
310
        let has_instructions = self
2✔
311
            .additional_files
2✔
312
            .iter()
2✔
313
            .any(|asset| asset.file_name == LESSON_INSTRUCTIONS_FILE);
2✔
314
        let has_material = self
2✔
315
            .additional_files
2✔
316
            .iter()
2✔
317
            .any(|asset| asset.file_name == LESSON_MATERIAL_FILE);
3✔
318
        let lesson_builder = LessonBuilder {
2✔
319
            lesson: KnowledgeBaseLesson {
2✔
320
                short_id: self.short_id,
2✔
321
                course_id,
2✔
322
                dependencies: self.dependencies.clone(),
2✔
323
                superseded: self.superseded.clone(),
2✔
324
                name: None,
2✔
325
                description: None,
2✔
326
                metadata: self.metadata.clone(),
2✔
327
                has_instructions,
2✔
328
                has_material,
2✔
329
            },
2✔
330
            exercises,
2✔
331
            asset_builders: self.additional_files.clone(),
2✔
332
        };
2✔
333
        Ok(lesson_builder)
2✔
334
    }
5✔
335
}
336

337
//@<simple-knowledge-base-course
338
/// Represents a simple knowledge base course which only specifies the course manifest and a list of
339
/// simple lessons. It is meant to help course authors write simple knowledge base courses by
340
/// writing a simple configuration to a single JSON file.
341
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
342
pub struct SimpleKnowledgeBaseCourse {
343
    /// The manifest for this course.
344
    pub manifest: CourseManifest,
345

346
    /// The simple lessons in this course.
347
    #[serde(default)]
348
    pub lessons: Vec<SimpleKnowledgeBaseLesson>,
349
}
350
//>@simple-knowledge-base-course
351

352
impl SimpleKnowledgeBaseCourse {
353
    /// Writes the course manifests and the lesson directories with the assets and exercises to the
354
    /// given directory.
355
    pub fn build(&self, root_directory: &Path) -> Result<()> {
5✔
356
        // Ensure that the lesson short IDs are unique.
5✔
357
        let mut short_ids = HashSet::new();
5✔
358
        for lesson in &self.lessons {
11✔
359
            ensure!(
7✔
360
                !short_ids.contains(&lesson.short_id),
7✔
361
                "short ID {} of lesson is not unique",
1✔
362
                lesson.short_id
363
            );
364
            short_ids.insert(&lesson.short_id);
6✔
365
        }
366

367
        // Generate the lesson builders.
368
        let lesson_builders = self
4✔
369
            .lessons
4✔
370
            .iter()
4✔
371
            .map(|lesson| lesson.generate_lesson_builder(self.manifest.id))
5✔
372
            .collect::<Result<Vec<_>>>()?;
4✔
373

374
        // Build the lessons in the course.
375
        for lesson_builder in lesson_builders {
3✔
376
            let lesson_directory = root_directory.join(format!(
2✔
377
                "{}{}",
2✔
378
                lesson_builder.lesson.short_id, LESSON_SUFFIX
2✔
379
            ));
2✔
380

2✔
381
            // Remove the lesson directories if they already exist.
2✔
382
            if lesson_directory.exists() {
2✔
383
                fs::remove_dir_all(&lesson_directory).context(format!(
1✔
384
                    "failed to remove existing lesson directory at {}",
1✔
385
                    lesson_directory.display()
1✔
386
                ))?;
1✔
387
            }
1✔
388

389
            lesson_builder.build(&lesson_directory)?;
2✔
390
        }
391

392
        // Write the course manifest.
393
        let manifest_path = root_directory.join(COURSE_MANIFEST_FILENAME);
1✔
394
        let display = manifest_path.display();
1✔
395
        let mut manifest_file = fs::File::create(&manifest_path).context(format!(
1✔
396
            "failed to create course manifest file at {display}"
1✔
397
        ))?;
1✔
398
        manifest_file
1✔
399
            .write_all(serde_json::to_string_pretty(&self.manifest)?.as_bytes())
1✔
400
            .context(format!("failed to write course manifest file at {display}"))
1✔
401
    }
5✔
402
}
403

404
#[cfg(test)]
405
mod test {
406
    use std::collections::BTreeMap;
407

408
    use anyhow::Result;
409

410
    use crate::{
411
        course_builder::knowledge_base_builder::*,
412
        data::{course_generator::knowledge_base::KnowledgeBaseFile, ExerciseType},
413
    };
414

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

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

1✔
499
        course_builder.build(temp_dir.path())?;
1✔
500

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

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

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

574
        Ok(())
1✔
575
    }
1✔
576

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

653
        // Create a temp directory and one of the lesson directories with some content to ensure
654
        // that is deleted. Then build the course and verify the contents of the output directory.
655
        let temp_dir = tempfile::tempdir()?;
1✔
656
        let dummy_dir = temp_dir.path().join("1.lesson").join("dummy");
1✔
657
        fs::create_dir_all(&dummy_dir)?;
1✔
658
        assert!(dummy_dir.exists());
1✔
659
        simple_course.build(temp_dir.path())?;
1✔
660
        assert!(!dummy_dir.exists());
1✔
661

662
        // Verify that the first lesson was built correctly.
663
        let lesson_dir = temp_dir.path().join("1.lesson");
1✔
664
        assert!(lesson_dir.exists());
1✔
665
        let front_file = lesson_dir.join("1.front.md");
1✔
666
        assert!(front_file.exists());
1✔
667
        assert_eq!(
1✔
668
            fs::read_to_string(&front_file)?,
1✔
669
            "Lesson 1, Exercise 1 front"
670
        );
671
        let front_file = lesson_dir.join("2.front.md");
1✔
672
        assert!(front_file.exists());
1✔
673
        assert_eq!(
1✔
674
            fs::read_to_string(&front_file)?,
1✔
675
            "Lesson 1, Exercise 2 front"
676
        );
677
        let dependencies_file = lesson_dir.join(LESSON_DEPENDENCIES_FILE);
1✔
678
        assert!(!dependencies_file.exists());
1✔
679
        let superseced_file = lesson_dir.join(LESSON_SUPERSEDED_FILE);
1✔
680
        assert!(!superseced_file.exists());
1✔
681
        let instructions_file = lesson_dir.join(LESSON_INSTRUCTIONS_FILE);
1✔
682
        assert!(!instructions_file.exists());
1✔
683
        let material_file = lesson_dir.join(LESSON_MATERIAL_FILE);
1✔
684
        assert!(!material_file.exists());
1✔
685

686
        // Verify that the second lesson was built correctly.
687
        let lesson_dir = temp_dir.path().join("2.lesson");
1✔
688
        assert!(lesson_dir.exists());
1✔
689
        let front_file = lesson_dir.join("1.front.md");
1✔
690
        assert!(front_file.exists());
1✔
691
        assert_eq!(
1✔
692
            fs::read_to_string(&front_file)?,
1✔
693
            "Lesson 2, Exercise 1 front"
694
        );
695
        let back_file = lesson_dir.join("1.back.md");
1✔
696
        assert!(back_file.exists());
1✔
697
        assert_eq!(fs::read_to_string(&back_file)?, "Lesson 2, Exercise 1 back");
1✔
698
        let front_file = lesson_dir.join("2.front.md");
1✔
699
        assert!(front_file.exists());
1✔
700
        assert_eq!(
1✔
701
            fs::read_to_string(&front_file)?,
1✔
702
            "Lesson 2, Exercise 2 front"
703
        );
704
        let back_file = lesson_dir.join("2.back.md");
1✔
705
        assert!(back_file.exists());
1✔
706
        assert_eq!(fs::read_to_string(&back_file)?, "Lesson 2, Exercise 2 back");
1✔
707
        let dependencies_file = lesson_dir.join(LESSON_DEPENDENCIES_FILE);
1✔
708
        assert!(dependencies_file.exists());
1✔
709
        assert_eq!(
1✔
710
            KnowledgeBaseFile::open::<Vec<String>>(&dependencies_file)?,
1✔
711
            vec!["1".to_string()]
1✔
712
        );
713
        let superseced_file = lesson_dir.join(LESSON_SUPERSEDED_FILE);
1✔
714
        assert!(superseced_file.exists());
1✔
715
        assert_eq!(
1✔
716
            KnowledgeBaseFile::open::<Vec<String>>(&superseced_file)?,
1✔
717
            vec!["0".to_string()]
1✔
718
        );
719
        let instructions_file = lesson_dir.join(LESSON_INSTRUCTIONS_FILE);
1✔
720
        assert!(instructions_file.exists());
1✔
721
        assert_eq!(
1✔
722
            fs::read_to_string(&instructions_file)?,
1✔
723
            "Lesson 2 instructions"
724
        );
725
        let material_file = lesson_dir.join(LESSON_MATERIAL_FILE);
1✔
726
        assert!(material_file.exists());
1✔
727
        assert_eq!(fs::read_to_string(&material_file)?, "Lesson 2 material");
1✔
728
        let metadata_file = lesson_dir.join(LESSON_METADATA_FILE);
1✔
729
        assert!(metadata_file.exists());
1✔
730
        assert_eq!(
1✔
731
            KnowledgeBaseFile::open::<BTreeMap<String, Vec<String>>>(&metadata_file)?,
1✔
732
            BTreeMap::from([("key".to_string(), vec!["value".to_string()])])
1✔
733
        );
734
        let dummy_file = lesson_dir.join("dummy.md");
1✔
735
        assert!(dummy_file.exists());
1✔
736
        assert_eq!(fs::read_to_string(&dummy_file)?, "I'm a dummy file");
1✔
737

738
        // Finally, clone the simple knowledge course to satisfy the code coverage check.
739
        assert_eq!(simple_course.clone(), simple_course);
1✔
740
        Ok(())
1✔
741
    }
1✔
742

743
    // Verifies that the simple knowledge course checks for duplicate lesson IDs.
744
    #[test]
745
    fn duplicate_short_lesson_ids() -> Result<()> {
1✔
746
        // Build a simple course with duplicate lesson IDs.
1✔
747
        let simple_course = SimpleKnowledgeBaseCourse {
1✔
748
            manifest: CourseManifest {
1✔
749
                id: "course1".into(),
1✔
750
                name: "Course 1".into(),
1✔
751
                dependencies: vec![],
1✔
752
                superseded: vec![],
1✔
753
                description: None,
1✔
754
                authors: None,
1✔
755
                metadata: None,
1✔
756
                course_material: None,
1✔
757
                course_instructions: None,
1✔
758
                generator_config: None,
1✔
759
            },
1✔
760
            lessons: vec![
1✔
761
                SimpleKnowledgeBaseLesson {
1✔
762
                    short_id: "1".into(),
1✔
763
                    dependencies: vec![],
1✔
764
                    superseded: vec![],
1✔
765
                    exercises: vec![SimpleKnowledgeBaseExercise {
1✔
766
                        short_id: "1".into(),
1✔
767
                        front: vec!["Lesson 1, Exercise 1 front".into()],
1✔
768
                        back: vec![],
1✔
769
                    }],
1✔
770
                    metadata: None,
1✔
771
                    additional_files: vec![],
1✔
772
                },
1✔
773
                SimpleKnowledgeBaseLesson {
1✔
774
                    short_id: "1".into(),
1✔
775
                    dependencies: vec![],
1✔
776
                    superseded: vec![],
1✔
777
                    exercises: vec![SimpleKnowledgeBaseExercise {
1✔
778
                        short_id: "1".into(),
1✔
779
                        front: vec!["Lesson 2, Exercise 1 front".into()],
1✔
780
                        back: vec![],
1✔
781
                    }],
1✔
782
                    metadata: None,
1✔
783
                    additional_files: vec![],
1✔
784
                },
1✔
785
            ],
1✔
786
        };
1✔
787

788
        // Verify that the course builder fails.
789
        let temp_dir = tempfile::tempdir()?;
1✔
790
        assert!(simple_course.build(temp_dir.path()).is_err());
1✔
791
        Ok(())
1✔
792
    }
1✔
793

794
    // Verifies that the simple knowledge course checks for duplicate exercise IDs.
795
    #[test]
796
    fn duplicate_short_exercise_ids() -> Result<()> {
1✔
797
        // Build a simple course with duplicate exercise IDs.
1✔
798
        let simple_course = SimpleKnowledgeBaseCourse {
1✔
799
            manifest: CourseManifest {
1✔
800
                id: "course1".into(),
1✔
801
                name: "Course 1".into(),
1✔
802
                dependencies: vec![],
1✔
803
                superseded: vec![],
1✔
804
                description: None,
1✔
805
                authors: None,
1✔
806
                metadata: None,
1✔
807
                course_material: None,
1✔
808
                course_instructions: None,
1✔
809
                generator_config: None,
1✔
810
            },
1✔
811
            lessons: vec![SimpleKnowledgeBaseLesson {
1✔
812
                short_id: "1".into(),
1✔
813
                dependencies: vec![],
1✔
814
                superseded: vec![],
1✔
815
                exercises: vec![
1✔
816
                    SimpleKnowledgeBaseExercise {
1✔
817
                        short_id: "1".into(),
1✔
818
                        front: vec!["Lesson 1, Exercise 1 front".into()],
1✔
819
                        back: vec![],
1✔
820
                    },
1✔
821
                    SimpleKnowledgeBaseExercise {
1✔
822
                        short_id: "1".into(),
1✔
823
                        front: vec!["Lesson 1, Exercise 2 front".into()],
1✔
824
                        back: vec![],
1✔
825
                    },
1✔
826
                ],
1✔
827
                metadata: None,
1✔
828
                additional_files: vec![],
1✔
829
            }],
1✔
830
        };
1✔
831

832
        // Verify that the course builder fails.
833
        let temp_dir = tempfile::tempdir()?;
1✔
834
        assert!(simple_course.build(temp_dir.path()).is_err());
1✔
835
        Ok(())
1✔
836
    }
1✔
837

838
    // Verifies that the simple knowledge course checks empty lesson IDs.
839
    #[test]
840
    fn empty_short_lesson_ids() -> Result<()> {
1✔
841
        // Build a simple course with empty lesson IDs.
1✔
842
        let simple_course = SimpleKnowledgeBaseCourse {
1✔
843
            manifest: CourseManifest {
1✔
844
                id: "course1".into(),
1✔
845
                name: "Course 1".into(),
1✔
846
                dependencies: vec![],
1✔
847
                superseded: vec![],
1✔
848
                description: None,
1✔
849
                authors: None,
1✔
850
                metadata: None,
1✔
851
                course_material: None,
1✔
852
                course_instructions: None,
1✔
853
                generator_config: None,
1✔
854
            },
1✔
855
            lessons: vec![SimpleKnowledgeBaseLesson {
1✔
856
                short_id: "".into(),
1✔
857
                dependencies: vec![],
1✔
858
                superseded: vec![],
1✔
859
                exercises: vec![SimpleKnowledgeBaseExercise {
1✔
860
                    short_id: "1".into(),
1✔
861
                    front: vec!["Lesson 1, Exercise 1 front".into()],
1✔
862
                    back: vec![],
1✔
863
                }],
1✔
864
                metadata: None,
1✔
865
                additional_files: vec![],
1✔
866
            }],
1✔
867
        };
1✔
868

869
        // Verify that the course builder fails.
870
        let temp_dir = tempfile::tempdir()?;
1✔
871
        assert!(simple_course.build(temp_dir.path()).is_err());
1✔
872
        Ok(())
1✔
873
    }
1✔
874

875
    // Verifies that the simple knowledge course checks empty exercise IDs.
876
    #[test]
877
    fn empty_short_exercise_ids() -> Result<()> {
1✔
878
        // Build a simple course with empty exercise IDs.
1✔
879
        let simple_course = SimpleKnowledgeBaseCourse {
1✔
880
            manifest: CourseManifest {
1✔
881
                id: "course1".into(),
1✔
882
                name: "Course 1".into(),
1✔
883
                dependencies: vec![],
1✔
884
                superseded: vec![],
1✔
885
                description: None,
1✔
886
                authors: None,
1✔
887
                metadata: None,
1✔
888
                course_material: None,
1✔
889
                course_instructions: None,
1✔
890
                generator_config: None,
1✔
891
            },
1✔
892
            lessons: vec![SimpleKnowledgeBaseLesson {
1✔
893
                short_id: "1".into(),
1✔
894
                dependencies: vec![],
1✔
895
                superseded: vec![],
1✔
896
                exercises: vec![SimpleKnowledgeBaseExercise {
1✔
897
                    short_id: String::new(),
1✔
898
                    front: vec!["Lesson 1, Exercise 1 front".into()],
1✔
899
                    back: vec![],
1✔
900
                }],
1✔
901
                metadata: None,
1✔
902
                additional_files: vec![],
1✔
903
            }],
1✔
904
        };
1✔
905

906
        // Verify that the course builder fails.
907
        let temp_dir = tempfile::tempdir()?;
1✔
908
        assert!(simple_course.build(temp_dir.path()).is_err());
1✔
909
        Ok(())
1✔
910
    }
1✔
911
}
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