Skip to content

Commit 0faa09b

Browse files
authored
Merge pull request #1081 from EnergySystemsModellingLab/regenerate-patch-test-data
Allow for regenerating test data for patched examples + tidy-ups
2 parents 25f3813 + 404ad00 commit 0faa09b

File tree

10 files changed

+290
-94
lines changed

10 files changed

+290
-94
lines changed

docs/developer_guide/architecture_quickstart.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,22 @@ just regenerate_test_data
108108
If you do so, please verify that the changes to the output files are at least roughly what was
109109
expected, before you commit these updated test files.
110110

111+
Note that if you only need to regenerate the data for some of the models, you can specify this with
112+
additional arguments, e.g.:
113+
114+
```sh
115+
just regenerate_test_data simple muse1_default
116+
```
117+
118+
This avoids regenerating data for other models unnecessarily, which can result in negligible
119+
differences in floating-point values in the output files.
120+
121+
If the model is a [patched example], then you need to pass the `--patch` flag, e.g.:
122+
123+
```sh
124+
just regenerate_test_data --patch simple_divisible
125+
```
126+
111127
[`log`]: https://docs.rs/log
112128
[`fern`]: https://docs.rs/fern
113129
[logging-docs]: ../user_guide.md#setting-the-log-level
@@ -117,6 +133,7 @@ expected, before you commit these updated test files.
117133
[test fixtures]: https://en.wikipedia.org/wiki/Test_fixture
118134
[`rstest`]: https://docs.rs/rstest
119135
[`fixture.rs`]: https://github.com/EnergySystemsModellingLab/MUSE2/blob/main/src/fixture.rs
136+
[patched example]: https://energysystemsmodellinglab.github.io/MUSE2/api/muse2/patch/index.html
120137

121138
## Example models
122139

@@ -158,7 +175,7 @@ For more information, consult [the documentation for the `units` module][units-m
158175
Input and output files for MUSE2 are either in [CSV] or [TOML] format. Users provide model
159176
definitions via a number of input files and the simulation results are written to files in an output
160177
folder. The code responsible for reading and validating input files and writing output files is in
161-
the [`input`][input-module] and [`output`][output-module], respectively.
178+
the [`input`][input-module] and [`output`][output-module] modules, respectively.
162179

163180
The file formats for MUSE2 input and output files are described [in the
164181
documentation][file-format-docs]. This documentation is generated from schema files ([JSON schemas]

justfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ coverage *ARGS:
1919
@cargo llvm-cov --html {{ARGS}}
2020

2121
# Regenerate data for regression tests
22-
regenerate_test_data:
23-
@tests/regenerate_all_data.sh
22+
regenerate_test_data *ARGS:
23+
@tests/regenerate_test_data.sh {{ARGS}}
2424

2525
# Run the pre-commit tool
2626
pre-commit *ARGS:

src/cli/example.rs

Lines changed: 89 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
//! Code related to the example models and the CLI commands for interacting with them.
22
use super::{RunOpts, handle_run_command};
3+
use crate::example::patches::{get_patch_names, get_patches};
4+
use crate::example::{Example, get_example_names};
5+
use crate::patch::ModelPatch;
36
use crate::settings::Settings;
4-
use anyhow::{Context, Result, ensure};
7+
use anyhow::{Context, Result};
58
use clap::Subcommand;
6-
use include_dir::{Dir, DirEntry, include_dir};
79
use std::fs;
810
use std::path::{Path, PathBuf};
911
use tempfile::TempDir;
1012

11-
/// The directory containing the example models.
12-
const EXAMPLES_DIR: Dir = include_dir!("examples");
13-
1413
/// The available subcommands for managing example models.
1514
#[derive(Subcommand)]
1615
pub enum ExampleSubcommands {
1716
/// List available examples.
18-
List,
17+
List {
18+
/// Whether to list patched models.
19+
#[arg(long, hide = true)]
20+
patch: bool,
21+
},
1922
/// Provide information about the specified example.
2023
Info {
2124
/// The name of the example.
@@ -27,11 +30,17 @@ pub enum ExampleSubcommands {
2730
name: String,
2831
/// The destination folder for the example.
2932
new_path: Option<PathBuf>,
33+
/// Whether the model to extract is a patched model.
34+
#[arg(long, hide = true)]
35+
patch: bool,
3036
},
3137
/// Run an example.
3238
Run {
3339
/// The name of the example to run.
3440
name: String,
41+
/// Whether the model to run is a patched model.
42+
#[arg(long, hide = true)]
43+
patch: bool,
3544
/// Other run options
3645
#[command(flatten)]
3746
opts: RunOpts,
@@ -42,81 +51,113 @@ impl ExampleSubcommands {
4251
/// Execute the supplied example subcommand
4352
pub fn execute(self) -> Result<()> {
4453
match self {
45-
Self::List => handle_example_list_command(),
54+
Self::List { patch } => handle_example_list_command(patch),
4655
Self::Info { name } => handle_example_info_command(&name)?,
4756
Self::Extract {
4857
name,
49-
new_path: dest,
50-
} => handle_example_extract_command(&name, dest.as_deref())?,
51-
Self::Run { name, opts } => handle_example_run_command(&name, &opts, None)?,
58+
patch,
59+
new_path,
60+
} => handle_example_extract_command(&name, new_path.as_deref(), patch)?,
61+
Self::Run { name, patch, opts } => {
62+
handle_example_run_command(&name, patch, &opts, None)?;
63+
}
5264
}
5365

5466
Ok(())
5567
}
5668
}
5769

5870
/// Handle the `example list` command.
59-
fn handle_example_list_command() {
60-
for entry in EXAMPLES_DIR.dirs() {
61-
println!("{}", entry.path().display());
71+
fn handle_example_list_command(patch: bool) {
72+
if patch {
73+
for name in get_patch_names() {
74+
println!("{name}");
75+
}
76+
} else {
77+
for name in get_example_names() {
78+
println!("{name}");
79+
}
6280
}
6381
}
6482

6583
/// Handle the `example info` command.
6684
fn handle_example_info_command(name: &str) -> Result<()> {
67-
let path: PathBuf = [name, "README.txt"].iter().collect();
68-
let readme = EXAMPLES_DIR
69-
.get_file(path)
70-
.context("Example not found.")?
71-
.contents_utf8()
72-
.expect("README.txt is not UTF-8 encoded");
73-
74-
print!("{readme}");
85+
// If we can't load it, it's a bug, hence why we panic
86+
let info = Example::from_name(name)?
87+
.get_readme()
88+
.unwrap_or_else(|_| panic!("Could not load README.txt for '{name}' example"));
89+
print!("{info}");
7590

7691
Ok(())
7792
}
7893

7994
/// Handle the `example extract` command
80-
fn handle_example_extract_command(name: &str, dest: Option<&Path>) -> Result<()> {
81-
let dest = dest.unwrap_or(Path::new(name));
82-
extract_example(name, dest)
95+
fn handle_example_extract_command(name: &str, dest: Option<&Path>, patch: bool) -> Result<()> {
96+
extract_example(name, patch, dest.unwrap_or(Path::new(name)))
8397
}
8498

85-
/// Extract the specified example to a new directory
86-
fn extract_example(name: &str, new_path: &Path) -> Result<()> {
87-
// Find the subdirectory in EXAMPLES_DIR whose name matches `name`.
88-
let sub_dir = EXAMPLES_DIR.get_dir(name).context("Example not found.")?;
99+
/// Extract the specified example to a new directory.
100+
///
101+
/// If `patch` is `true`, then the corresponding patched example will be extracted.
102+
fn extract_example(name: &str, patch: bool, dest: &Path) -> Result<()> {
103+
if patch {
104+
let patches = get_patches(name)?;
89105

90-
ensure!(
91-
!new_path.exists(),
92-
"Destination directory {} already exists",
93-
new_path.display()
94-
);
106+
// NB: All patched models are based on `simple`, for now
107+
let example = Example::from_name("simple").unwrap();
95108

96-
// Copy the contents of the subdirectory to the destination
97-
fs::create_dir(new_path)?;
98-
for entry in sub_dir.entries() {
99-
match entry {
100-
DirEntry::Dir(_) => panic!("Subdirectories in examples not supported"),
101-
DirEntry::File(f) => {
102-
let file_name = f.path().file_name().unwrap();
103-
let file_path = new_path.join(file_name);
104-
fs::write(&file_path, f.contents())?;
105-
}
106-
}
107-
}
109+
// First extract the example to a temp dir
110+
let example_tmp = TempDir::new().context("Failed to create temporary directory")?;
111+
let example_path = example_tmp.path().join(name);
112+
example
113+
.extract(&example_path)
114+
.context("Could not extract example")?;
108115

109-
Ok(())
116+
// Patch example and put contents in dest
117+
fs::create_dir(dest).context("Could not create output directory")?;
118+
ModelPatch::new(example_path)
119+
.with_file_patches(patches.to_owned())
120+
.build(dest)
121+
.context("Failed to patch example")
122+
} else {
123+
// Otherwise it's just a regular example
124+
let example = Example::from_name(name)?;
125+
example.extract(dest)
126+
}
110127
}
111128

112129
/// Handle the `example run` command.
113130
pub fn handle_example_run_command(
114131
name: &str,
132+
patch: bool,
115133
opts: &RunOpts,
116134
settings: Option<Settings>,
117135
) -> Result<()> {
118-
let temp_dir = TempDir::new().context("Failed to create temporary directory.")?;
136+
let temp_dir = TempDir::new().context("Failed to create temporary directory")?;
119137
let model_path = temp_dir.path().join(name);
120-
extract_example(name, &model_path)?;
138+
extract_example(name, patch, &model_path)?;
121139
handle_run_command(&model_path, opts, settings)
122140
}
141+
142+
#[cfg(test)]
143+
mod tests {
144+
use super::*;
145+
use rstest::rstest;
146+
147+
fn assert_dir_non_empty(path: &Path) {
148+
assert!(
149+
path.read_dir().unwrap().next().is_some(),
150+
"Directory is empty"
151+
);
152+
}
153+
154+
#[rstest]
155+
#[case("muse1_default", false)]
156+
#[case("simple_divisible", true)]
157+
fn check_extract_example(#[case] name: &str, #[case] patch: bool) {
158+
let tmp = TempDir::new().unwrap();
159+
let dest = tmp.path().join("out");
160+
extract_example(name, patch, &dest).unwrap();
161+
assert_dir_non_empty(&dest);
162+
}
163+
}

src/example.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//! Code for working with example models
2+
use std::fs;
3+
use std::path::Path;
4+
5+
use anyhow::{Context, Result};
6+
use include_dir::{Dir, DirEntry, include_dir};
7+
8+
pub mod patches;
9+
10+
/// The directory containing the example models.
11+
const EXAMPLES_DIR: Dir = include_dir!("examples");
12+
13+
/// Get the names of all examples
14+
pub fn get_example_names() -> impl Iterator<Item = &'static str> {
15+
EXAMPLES_DIR.dirs().map(|dir| {
16+
dir.path()
17+
.as_os_str()
18+
.to_str()
19+
.expect("Invalid unicode in path")
20+
})
21+
}
22+
23+
/// A bundled example model
24+
pub struct Example(Dir<'static>);
25+
26+
impl Example {
27+
/// Get the example with the specified name
28+
pub fn from_name(name: &str) -> Result<Self> {
29+
let dir = EXAMPLES_DIR
30+
.get_dir(name)
31+
.with_context(|| format!("Example '{name}' not found"))?;
32+
33+
Ok(Self(dir.clone()))
34+
}
35+
36+
/// Get the contents of the readme file for this example
37+
pub fn get_readme(&self) -> Result<&'static str> {
38+
self.0
39+
.get_file(self.0.path().join("README.txt"))
40+
.context("Missing file")?
41+
.contents_utf8()
42+
.context("File not UTF-8 encoded")
43+
}
44+
45+
/// Extract this example to a specified destination.
46+
///
47+
/// Returns an error if the destination directory already exists or copying the files fails.
48+
pub fn extract(&self, new_path: &Path) -> Result<()> {
49+
// Copy the contents of the subdirectory to the destination
50+
fs::create_dir(new_path)?;
51+
for entry in self.0.entries() {
52+
match entry {
53+
DirEntry::Dir(_) => panic!("Subdirectories in examples not supported"),
54+
DirEntry::File(f) => {
55+
let file_name = f.path().file_name().unwrap();
56+
let file_path = new_path.join(file_name);
57+
fs::write(&file_path, f.contents())?;
58+
}
59+
}
60+
}
61+
62+
Ok(())
63+
}
64+
}
65+
66+
#[cfg(test)]
67+
mod tests {
68+
use super::*;
69+
70+
#[test]
71+
fn all_examples_have_readme() {
72+
for example in get_example_names() {
73+
let readme = Example::from_name(example)
74+
.unwrap()
75+
.get_readme()
76+
.with_context(|| format!("Could not load readme for {example}"))
77+
.unwrap();
78+
79+
assert!(!readme.trim().is_empty());
80+
}
81+
}
82+
}

src/example/patches.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//! File patches to be used in integration tests.
2+
//!
3+
//! This is used to test small variations on existing example models.
4+
use crate::patch::FilePatch;
5+
use anyhow::{Context, Result};
6+
use std::{collections::BTreeMap, sync::LazyLock};
7+
8+
/// A map of file patches, keyed by name
9+
type PatchMap = BTreeMap<&'static str, Vec<FilePatch>>;
10+
11+
/// The file patches, keyed by name
12+
static PATCHES: LazyLock<PatchMap> = LazyLock::new(get_all_patches);
13+
14+
/// Get all patches
15+
fn get_all_patches() -> PatchMap {
16+
[(
17+
// The simple example with gas boiler process made divisible
18+
"simple_divisible",
19+
vec![
20+
FilePatch::new("processes.csv")
21+
.with_deletion("RGASBR,Gas boiler,all,RSHEAT,2020,2040,1.0,")
22+
.with_addition("RGASBR,Gas boiler,all,RSHEAT,2020,2040,1.0,1000"),
23+
],
24+
)]
25+
.into_iter()
26+
.collect()
27+
}
28+
29+
/// Get the names for all the patches
30+
pub fn get_patch_names() -> impl Iterator<Item = &'static str> {
31+
PATCHES.keys().copied()
32+
}
33+
34+
/// Get patches for the named patched example
35+
pub fn get_patches(name: &str) -> Result<&[FilePatch]> {
36+
Ok(PATCHES
37+
.get(name)
38+
.with_context(|| format!("Patched example '{name}' not found"))?)
39+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub mod agent;
1111
pub mod asset;
1212
pub mod cli;
1313
pub mod commodity;
14+
pub mod example;
1415
pub mod finance;
1516
pub mod graph;
1617
pub mod id;

0 commit comments

Comments
 (0)