• 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

92.77
/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
pub mod music;
12

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

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

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

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

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

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

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

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

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

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

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

78
impl ExerciseBuilder {
79
    /// Writes the files needed for this exercise to the given directory.
80
    pub fn build(
12,677✔
81
        &self,
12,677✔
82
        exercise_directory: &PathBuf,
12,677✔
83
        manifest_template: ExerciseManifestBuilder,
12,677✔
84
    ) -> Result<()> {
12,677✔
85
        // Verify that the directory doesn't already exist and create it.
12,677✔
86
        ensure!(
12,677✔
87
            !exercise_directory.is_dir(),
12,677✔
UNCOV
88
            "exercise directory {} already exists",
×
NEW
89
            exercise_directory.display(),
×
90
        );
91
        create_dir_all(exercise_directory)?;
12,677✔
92

93
        // Write the exercise manifest.
94
        let manifest = (self.manifest_closure)(manifest_template).build()?;
12,677✔
95
        let manifest_json = serde_json::to_string_pretty(&manifest)? + "\n";
12,677✔
96
        let manifest_path = exercise_directory.join("exercise_manifest.json");
12,677✔
97
        let mut manifest_file = File::create(manifest_path)?;
12,677✔
98
        manifest_file.write_all(manifest_json.as_bytes())?;
12,677✔
99

100
        // Write all the assets.
101
        for asset_builder in &self.asset_builders {
38,029✔
102
            asset_builder.build(exercise_directory)?;
25,352✔
103
        }
104

105
        // Verify that all paths mentioned in the manifest are valid.
106
        ensure!(
12,677✔
107
            manifest.verify_paths(exercise_directory)?,
12,677✔
UNCOV
108
            "cannot verify files mentioned in the manifest for exercise {}",
×
109
            manifest.id,
110
        );
111
        Ok(())
12,677✔
112
    }
12,677✔
113
}
114

115
/// A builder that generates the files needed to add a lesson to a course.
116
pub struct LessonBuilder {
117
    /// Base name of the directory on which to store this lesson.
118
    pub directory_name: String,
119

120
    /// A closure taking a builder common to all lessons which returns the builder for a specific
121
    /// lesson manifest.
122
    pub manifest_closure: Box<dyn Fn(LessonManifestBuilder) -> LessonManifestBuilder>,
123

124
    /// A template builder used to build the manifests for each exercise in the lesson. Common
125
    /// attributes to all exercises should be set here.
126
    pub exercise_manifest_template: ExerciseManifestBuilder,
127

128
    /// A list of tuples of exercise directory name and exercise builder to create the exercises in
129
    /// the lesson.
130
    pub exercise_builders: Vec<ExerciseBuilder>,
131

132
    /// A list of asset builders to create assets specific to this lesson.
133
    pub asset_builders: Vec<AssetBuilder>,
134
}
135

136
impl LessonBuilder {
137
    /// Writes the files needed for this lesson to the given directory.
138
    pub fn build(
1,354✔
139
        &self,
1,354✔
140
        lesson_directory: &PathBuf,
1,354✔
141
        manifest_template: LessonManifestBuilder,
1,354✔
142
    ) -> Result<()> {
1,354✔
143
        // Verify that the directory doesn't already exist and create it.
1,354✔
144
        ensure!(
1,354✔
145
            !lesson_directory.is_dir(),
1,354✔
UNCOV
146
            "lesson directory {} already exists",
×
NEW
147
            lesson_directory.display(),
×
148
        );
149
        create_dir_all(lesson_directory)?;
1,354✔
150

151
        // Write the lesson manifest.
152
        let manifest = (self.manifest_closure)(manifest_template).build()?;
1,354✔
153
        let manifest_json = serde_json::to_string_pretty(&manifest)? + "\n";
1,354✔
154
        let manifest_path = lesson_directory.join("lesson_manifest.json");
1,354✔
155
        let mut manifest_file = File::create(manifest_path)?;
1,354✔
156
        manifest_file.write_all(manifest_json.as_bytes())?;
1,354✔
157

158
        // Write all the assets.
159
        for asset_builder in &self.asset_builders {
4,060✔
160
            asset_builder.build(lesson_directory)?;
2,706✔
161
        }
162

163
        // Build all the exercises in the lesson.
164
        for exercise_builder in &self.exercise_builders {
14,031✔
165
            let exercise_directory = lesson_directory.join(&exercise_builder.directory_name);
12,677✔
166
            exercise_builder.build(&exercise_directory, self.exercise_manifest_template.clone())?;
12,677✔
167
        }
168

169
        // Verify that all paths mentioned in the manifest are valid.
170
        ensure!(
1,354✔
171
            manifest.verify_paths(lesson_directory)?,
1,354✔
UNCOV
172
            "cannot verify files mentioned in the manifest for lesson {}",
×
173
            manifest.id,
174
        );
175
        Ok(())
1,354✔
176
    }
1,354✔
177
}
178

179
/// A builder that generates the files needed to add a course.
180
pub struct CourseBuilder {
181
    /// Base name of the directory on which to store this course.
182
    pub directory_name: String,
183

184
    /// The manifest for the course.
185
    pub course_manifest: CourseManifest,
186

187
    /// A template builder used to build the manifests for each lesson in the course. Attributes
188
    /// common to all lessons should be set here.
189
    pub lesson_manifest_template: LessonManifestBuilder,
190

191
    /// A list of tuples of directory names and lesson builders to create the lessons in the
192
    /// course.
193
    pub lesson_builders: Vec<LessonBuilder>,
194

195
    /// A list of asset builders to create assets specific to this course.
196
    pub asset_builders: Vec<AssetBuilder>,
197
}
198

199
impl CourseBuilder {
200
    /// Writes the files needed for this course to the given directory.
201
    pub fn build(&self, parent_directory: &Path) -> Result<()> {
479✔
202
        // Verify that the directory doesn't already exist and create it.
479✔
203
        let course_directory = parent_directory.join(&self.directory_name);
479✔
204
        ensure!(
479✔
205
            !course_directory.is_dir(),
479✔
UNCOV
206
            "course directory {} already exists",
×
NEW
207
            course_directory.display(),
×
208
        );
209
        create_dir_all(&course_directory)?;
479✔
210

211
        // Write the course manifest.
212
        let manifest_json = serde_json::to_string_pretty(&self.course_manifest)? + "\n";
479✔
213
        let manifest_path = course_directory.join("course_manifest.json");
479✔
214
        let mut manifest_file = File::create(manifest_path)?;
479✔
215
        manifest_file.write_all(manifest_json.as_bytes())?;
479✔
216

217
        // Write all the assets.
218
        for asset_builder in &self.asset_builders {
1,438✔
219
            asset_builder.build(&course_directory)?;
959✔
220
        }
221

222
        // Build all the lessons in the course.
223
        for lesson_builder in &self.lesson_builders {
1,833✔
224
            let lesson_directory = course_directory.join(&lesson_builder.directory_name);
1,354✔
225
            lesson_builder.build(&lesson_directory, self.lesson_manifest_template.clone())?;
1,354✔
226
        }
227

228
        // Verify that all paths mentioned in the manifest are valid.
229
        ensure!(
479✔
230
            self.course_manifest
479✔
231
                .verify_paths(course_directory.as_path())?,
479✔
UNCOV
232
            "cannot verify files mentioned in the manifest for course {}",
×
233
            self.course_manifest.id,
234
        );
235
        Ok(())
479✔
236
    }
479✔
237
}
238

239
#[cfg(test)]
240
mod test {
241
    use anyhow::Result;
242
    use std::io::Read;
243

244
    use super::*;
245
    use crate::data::{BasicAsset, ExerciseAsset, ExerciseType};
246

247
    /// Verifies the asset builder writes the contents to the correct file.
248
    #[test]
249
    fn asset_builer() -> Result<()> {
1✔
250
        let temp_dir = tempfile::tempdir()?;
1✔
251
        let asset_builder = AssetBuilder {
1✔
252
            file_name: "asset1.md".to_string(),
1✔
253
            contents: "asset1 contents".to_string(),
1✔
254
        };
1✔
255
        asset_builder.build(temp_dir.path())?;
1✔
256
        assert!(temp_dir.path().join("asset1.md").is_file());
1✔
257
        let mut file = File::open(temp_dir.path().join("asset1.md"))?;
1✔
258
        let mut contents = String::new();
1✔
259
        file.read_to_string(&mut contents)?;
1✔
260
        assert_eq!(contents, "asset1 contents");
1✔
261
        Ok(())
1✔
262
    }
1✔
263

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

320
        let temp_dir = tempfile::tempdir()?;
1✔
321
        course_builder.build(temp_dir.path())?;
1✔
322

323
        let course_dir = temp_dir.path().join("course1");
1✔
324
        let lesson_dir = course_dir.join("lesson1");
1✔
325
        let exercise_dir = lesson_dir.join("exercise1");
1✔
326
        assert!(course_dir.is_dir());
1✔
327
        assert!(lesson_dir.is_dir());
1✔
328
        assert!(exercise_dir.is_dir());
1✔
329
        assert!(course_dir.join("course_manifest.json").is_file());
1✔
330
        assert!(lesson_dir.join("lesson_manifest.json").is_file());
1✔
331
        assert!(exercise_dir.join("exercise_manifest.json").is_file());
1✔
332
        Ok(())
1✔
333
    }
1✔
334
}
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