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

trane-project / trane / 14392837286

11 Apr 2025 12:23AM UTC coverage: 99.221% (-0.4%) from 99.655%
14392837286

Pull #340

github

web-flow
Merge 48f74d0a2 into e7a732a69
Pull Request #340: Fixes to support external applications

24 of 29 new or added lines in 3 files covered. (82.76%)

19 existing lines in 4 files now uncovered.

5478 of 5521 relevant lines covered (99.22%)

172767.94 hits per line

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

97.33
/src/course_builder.rs
1
//! Defines utilities to make it easier to generate courses and lessons.
2
//!
3
//! Courses, lessons, and exercises are stored in JSON files that are the serialized versions of the
4
//! manifests in the `data` module. This means that writers of Trane courses can simply generate the
5
//! files by hand. However, this process is tedious and error-prone, so this module provides
6
//! utilities to make it easier to generate these files. In addition, Trane is in early stages of
7
//! development, so the format of the manifests is not stable yet. Generating the files by code
8
//! makes it easier to make updates to the files as the format changes.
9

10
pub mod knowledge_base_builder;
11
#[cfg_attr(coverage, coverage(off))]
12
pub mod music;
13

14
use anyhow::{Context, Result, ensure};
15
use serde::{Deserialize, Serialize};
16
use std::{
17
    fs::{File, create_dir_all},
18
    io::Write,
19
    path::{Path, PathBuf},
20
};
21
use strum::Display;
22

23
use crate::data::{CourseManifest, ExerciseManifestBuilder, LessonManifestBuilder, VerifyPaths};
24

25
/// Common metadata keys for all courses and lessons.
26
#[derive(Display)]
1✔
27
#[strum(serialize_all = "snake_case")]
28
#[allow(missing_docs)]
29
pub enum TraneMetadata {
30
    Skill,
31
}
32

33
/// A builder to generate plain-text asset files.
34
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35
pub struct AssetBuilder {
36
    /// The name of the file, which will be joined with the directory passed in the build function.
37
    pub file_name: String,
38

39
    /// The contents of the file as a string.
40
    pub contents: String,
41
}
42

43
impl AssetBuilder {
44
    /// Writes the asset to the given directory.
45
    pub fn build(&self, asset_directory: &Path) -> Result<()> {
29,926✔
46
        // Create the asset directory and verify there's not an existing file with the same name.
29,926✔
47
        create_dir_all(asset_directory)?;
29,926✔
48
        let asset_path = asset_directory.join(&self.file_name);
29,926✔
49
        ensure!(
29,926✔
50
            !asset_path.exists(),
29,926✔
51
            "asset path {} already exists",
1✔
52
            asset_path.display()
1✔
53
        );
54

55
        // Create any parent directories to the asset path to support specifying a directory in the
56
        // asset path.
57
        create_dir_all(asset_path.parent().unwrap())?;
29,925✔
58

59
        // Write the asset file.
60
        let mut asset_file = File::create(asset_path)?;
29,925✔
61
        asset_file.write_all(self.contents.as_bytes())?;
29,925✔
62
        Ok(())
29,925✔
63
    }
29,926✔
64
}
65

66
/// A builder that generates all the files needed to add an exercise to a lesson.
67
pub struct ExerciseBuilder {
68
    /// The base name of the directory on which to store this lesson.
69
    pub directory_name: String,
70

71
    /// A closure taking a builder common to all exercises which returns the builder for a specific
72
    /// exercise manifest.
73
    pub manifest_closure: Box<dyn Fn(ExerciseManifestBuilder) -> ExerciseManifestBuilder>,
74

75
    /// A list of asset builders to create assets specific to this exercise.
76
    pub asset_builders: Vec<AssetBuilder>,
77
}
78

79
impl ExerciseBuilder {
80
    /// Writes the files needed for this exercise to the given directory.
81
    pub fn build(
13,006✔
82
        &self,
13,006✔
83
        exercise_directory: &PathBuf,
13,006✔
84
        manifest_template: ExerciseManifestBuilder,
13,006✔
85
    ) -> Result<()> {
13,006✔
86
        // Create the directory and write the exercise manifest.
13,006✔
87
        create_dir_all(exercise_directory)?;
13,006✔
88
        let manifest = (self.manifest_closure)(manifest_template).build()?;
13,006✔
89
        let manifest_json = serde_json::to_string_pretty(&manifest)? + "\n";
13,006✔
90
        let manifest_path = exercise_directory.join("exercise_manifest.json");
13,006✔
91
        let mut manifest_file = File::create(manifest_path)?;
13,006✔
92
        manifest_file.write_all(manifest_json.as_bytes())?;
13,006✔
93

94
        // Write all the assets.
95
        for asset_builder in &self.asset_builders {
39,016✔
96
            asset_builder.build(exercise_directory)?;
26,010✔
97
        }
98

99
        // Verify that all paths mentioned in the manifest are valid.
100
        manifest.verify_paths(exercise_directory).context(format!(
13,006✔
101
            "failed to verify files for exercise {}",
13,006✔
102
            manifest.id
UNCOV
103
        ))?;
×
104
        Ok(())
13,006✔
105
    }
13,006✔
106
}
107

108
/// A builder that generates the files needed to add a lesson to a course.
109
pub struct LessonBuilder {
110
    /// Base name of the directory on which to store this lesson.
111
    pub directory_name: String,
112

113
    /// A closure taking a builder common to all lessons which returns the builder for a specific
114
    /// lesson manifest.
115
    pub manifest_closure: Box<dyn Fn(LessonManifestBuilder) -> LessonManifestBuilder>,
116

117
    /// A template builder used to build the manifests for each exercise in the lesson. Common
118
    /// attributes to all exercises should be set here.
119
    pub exercise_manifest_template: ExerciseManifestBuilder,
120

121
    /// A list of tuples of exercise directory name and exercise builder to create the exercises in
122
    /// the lesson.
123
    pub exercise_builders: Vec<ExerciseBuilder>,
124

125
    /// A list of asset builders to create assets specific to this lesson.
126
    pub asset_builders: Vec<AssetBuilder>,
127
}
128

129
impl LessonBuilder {
130
    /// Writes the files needed for this lesson to the given directory.
131
    pub fn build(
1,391✔
132
        &self,
1,391✔
133
        lesson_directory: &PathBuf,
1,391✔
134
        manifest_template: LessonManifestBuilder,
1,391✔
135
    ) -> Result<()> {
1,391✔
136
        // Create the directory and write the lesson manifest.
1,391✔
137
        create_dir_all(lesson_directory)?;
1,391✔
138
        let manifest = (self.manifest_closure)(manifest_template).build()?;
1,391✔
139
        let manifest_json = serde_json::to_string_pretty(&manifest)? + "\n";
1,391✔
140
        let manifest_path = lesson_directory.join("lesson_manifest.json");
1,391✔
141
        let mut manifest_file = File::create(manifest_path)?;
1,391✔
142
        manifest_file.write_all(manifest_json.as_bytes())?;
1,391✔
143

144
        // Write all the assets.
145
        for asset_builder in &self.asset_builders {
4,171✔
146
            asset_builder.build(lesson_directory)?;
2,780✔
147
        }
148

149
        // Build all the exercises in the lesson.
150
        for exercise_builder in &self.exercise_builders {
14,397✔
151
            let exercise_directory = lesson_directory.join(&exercise_builder.directory_name);
13,006✔
152
            exercise_builder.build(&exercise_directory, self.exercise_manifest_template.clone())?;
13,006✔
153
        }
154

155
        // Verify that all paths mentioned in the manifest are valid.
156
        manifest
1,391✔
157
            .verify_paths(lesson_directory)
1,391✔
158
            .context(format!("failed to verify files for lesson {}", manifest.id))?;
1,391✔
159
        Ok(())
1,391✔
160
    }
1,391✔
161
}
162

163
/// A builder that generates the files needed to add a course.
164
pub struct CourseBuilder {
165
    /// Base name of the directory on which to store this course.
166
    pub directory_name: String,
167

168
    /// The manifest for the course.
169
    pub course_manifest: CourseManifest,
170

171
    /// A template builder used to build the manifests for each lesson in the course. Attributes
172
    /// common to all lessons should be set here.
173
    pub lesson_manifest_template: LessonManifestBuilder,
174

175
    /// A list of tuples of directory names and lesson builders to create the lessons in the
176
    /// course.
177
    pub lesson_builders: Vec<LessonBuilder>,
178

179
    /// A list of asset builders to create assets specific to this course.
180
    pub asset_builders: Vec<AssetBuilder>,
181
}
182

183
impl CourseBuilder {
184
    /// Writes the files needed for this course to the given directory.
185
    pub fn build(&self, parent_directory: &Path) -> Result<()> {
479✔
186
        // Create the directory and write the course manifest.
479✔
187
        let course_directory = parent_directory.join(&self.directory_name);
479✔
188
        create_dir_all(&course_directory)?;
479✔
189
        let manifest_json = serde_json::to_string_pretty(&self.course_manifest)? + "\n";
479✔
190
        let manifest_path = course_directory.join("course_manifest.json");
479✔
191
        let mut manifest_file = File::create(manifest_path)?;
479✔
192
        manifest_file.write_all(manifest_json.as_bytes())?;
479✔
193

194
        // Write all the assets.
195
        for asset_builder in &self.asset_builders {
1,438✔
196
            asset_builder.build(&course_directory)?;
959✔
197
        }
198

199
        // Build all the lessons in the course.
200
        for lesson_builder in &self.lesson_builders {
1,870✔
201
            let lesson_directory = course_directory.join(&lesson_builder.directory_name);
1,391✔
202
            lesson_builder.build(&lesson_directory, self.lesson_manifest_template.clone())?;
1,391✔
203
        }
204

205
        // Verify that all paths mentioned in the manifest are valid.
206
        self.course_manifest
479✔
207
            .verify_paths(&course_directory)
479✔
208
            .context(format!(
479✔
209
                "failed to verify files for course {}",
479✔
210
                self.course_manifest.id
UNCOV
211
            ))?;
×
212
        Ok(())
479✔
213
    }
479✔
214
}
215

216
#[cfg(test)]
217
#[cfg_attr(coverage, coverage(off))]
218
mod test {
219
    use anyhow::Result;
220
    use std::io::Read;
221

222
    use super::*;
223
    use crate::data::{BasicAsset, ExerciseAsset, ExerciseType};
224

225
    /// Verifies the asset builder writes the contents to the correct file.
226
    #[test]
227
    fn asset_builer() -> Result<()> {
228
        let temp_dir = tempfile::tempdir()?;
229
        let asset_builder = AssetBuilder {
230
            file_name: "asset1.md".to_string(),
231
            contents: "asset1 contents".to_string(),
232
        };
233
        asset_builder.build(temp_dir.path())?;
234
        assert!(temp_dir.path().join("asset1.md").is_file());
235
        let mut file = File::open(temp_dir.path().join("asset1.md"))?;
236
        let mut contents = String::new();
237
        file.read_to_string(&mut contents)?;
238
        assert_eq!(contents, "asset1 contents");
239
        Ok(())
240
    }
241

242
    /// Verifies the asset builder fails if there's an existing file.
243
    #[test]
244
    fn asset_builer_existing() -> Result<()> {
245
        // Create the file first.
246
        let temp_dir = tempfile::tempdir()?;
247
        let asset_path = temp_dir.path().join("asset1.md");
248
        File::create(&asset_path)?;
249

250
        // Creating the asset builder should fail.
251
        let asset_builder = AssetBuilder {
252
            file_name: "asset1.md".to_string(),
253
            contents: "asset1 contents".to_string(),
254
        };
255
        assert!(asset_builder.build(temp_dir.path()).is_err());
256
        Ok(())
257
    }
258

259
    /// Verifies the course builder writes the correct files.
260
    #[test]
261
    fn course_builder() -> Result<()> {
262
        let exercise_builder = ExerciseBuilder {
263
            directory_name: "exercise1".to_string(),
264
            manifest_closure: Box::new(|builder| {
265
                builder
266
                    .clone()
267
                    .id("exercise1")
268
                    .name("Exercise 1".into())
269
                    .exercise_asset(ExerciseAsset::BasicAsset(BasicAsset::InlinedAsset {
270
                        content: String::new(),
271
                    }))
272
                    .clone()
273
            }),
274
            asset_builders: vec![],
275
        };
276
        let lesson_builder = LessonBuilder {
277
            directory_name: "lesson1".to_string(),
278
            manifest_closure: Box::new(|builder| {
279
                builder
280
                    .clone()
281
                    .id("lesson1")
282
                    .name("Lesson 1".into())
283
                    .dependencies(vec![])
284
                    .clone()
285
            }),
286
            exercise_manifest_template: ExerciseManifestBuilder::default()
287
                .lesson_id("lesson1")
288
                .course_id("course1")
289
                .exercise_type(ExerciseType::Procedural)
290
                .clone(),
291
            exercise_builders: vec![exercise_builder],
292
            asset_builders: vec![],
293
        };
294
        let course_builder = CourseBuilder {
295
            directory_name: "course1".to_string(),
296
            course_manifest: CourseManifest {
297
                id: "course1".into(),
298
                name: "Course 1".into(),
299
                dependencies: vec![],
300
                superseded: vec![],
301
                description: None,
302
                authors: None,
303
                metadata: None,
304
                course_material: None,
305
                course_instructions: None,
306
                generator_config: None,
307
            },
308
            lesson_manifest_template: LessonManifestBuilder::default()
309
                .course_id("course1")
310
                .clone(),
311
            lesson_builders: vec![lesson_builder],
312
            asset_builders: vec![],
313
        };
314

315
        let temp_dir = tempfile::tempdir()?;
316
        course_builder.build(temp_dir.path())?;
317

318
        let course_dir = temp_dir.path().join("course1");
319
        let lesson_dir = course_dir.join("lesson1");
320
        let exercise_dir = lesson_dir.join("exercise1");
321
        assert!(course_dir.is_dir());
322
        assert!(lesson_dir.is_dir());
323
        assert!(exercise_dir.is_dir());
324
        assert!(course_dir.join("course_manifest.json").is_file());
325
        assert!(lesson_dir.join("lesson_manifest.json").is_file());
326
        assert!(exercise_dir.join("exercise_manifest.json").is_file());
327
        Ok(())
328
    }
329

330
    /// Tests the Display implementation of TraneMetadata to satisfy coverage.
331
    #[test]
332
    fn trane_metadata_display() {
333
        assert_eq!("skill", TraneMetadata::Skill.to_string());
334
    }
335
}
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