Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ edit = "0.1.5"
erased-serde = "0.4.9"

[dev-dependencies]
assert_cmd = "2.1.2"
map-macro = "0.3.0"
rstest = {version = "0.26.1", default-features = false, features = ["crate-name"]}
yaml-rust2 = {version = "0.11.0", default-features = false}
Expand Down
5 changes: 2 additions & 3 deletions docs/developer_guide/architecture_quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,11 @@ bundled with the MUSE2 executable ([see user guide for more detail][user-guide-e
As these are intended as both a kind of documentation and templates, they should ideally be kept as
simple as possible.

If you add a new example model, please also add a regression test ([see here for an
example][regression-test-example]).
If you add a new example model, please also add a regression test to [`tests/regression.rs`].

[`examples`]: https://github.com/EnergySystemsModellingLab/MUSE2/blob/main/examples/
[user-guide-example-models]: https://energysystemsmodellinglab.github.io/MUSE2/user_guide.html#example-models
[regression-test-example]: https://github.com/EnergySystemsModellingLab/MUSE2/blob/main/tests/regression_muse1_default.rs
[`tests/regression.rs`]: https://github.com/EnergySystemsModellingLab/MUSE2/blob/main/tests/regression.rs

## Unit types

Expand Down
51 changes: 13 additions & 38 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ pub struct RunOpts {
#[arg(long)]
pub overwrite: bool,
/// Whether to write additional information to CSV files
#[arg(long, value_name = "BOOL", num_args = 0..=1, default_missing_value = "true")]
pub debug_model: Option<bool>,
#[arg(long)]
pub debug_model: bool,
}

/// Options for the `graph` command
Expand Down Expand Up @@ -94,12 +94,10 @@ impl Commands {
/// Execute the supplied CLI command
fn execute(self) -> Result<()> {
match self {
Self::Run { model_dir, opts } => handle_run_command(&model_dir, &opts, None),
Self::Run { model_dir, opts } => handle_run_command(&model_dir, &opts),
Self::Example { subcommand } => subcommand.execute(),
Self::Validate { model_dir } => handle_validate_command(&model_dir, None),
Self::SaveGraphs { model_dir, opts } => {
handle_save_graphs_command(&model_dir, &opts, None)
}
Self::Validate { model_dir } => handle_validate_command(&model_dir),
Self::SaveGraphs { model_dir, opts } => handle_save_graphs_command(&model_dir, &opts),
Self::Settings { subcommand } => subcommand.execute(),
}
}
Expand All @@ -126,21 +124,12 @@ pub fn run_cli() -> Result<()> {
}

/// Handle the `run` command.
pub fn handle_run_command(
model_path: &Path,
opts: &RunOpts,
settings: Option<Settings>,
) -> Result<()> {
// Load program settings, if not provided
let mut settings = if let Some(settings) = settings {
settings
} else {
Settings::load().context("Failed to load settings.")?
};
pub fn handle_run_command(model_path: &Path, opts: &RunOpts) -> Result<()> {
let mut settings = Settings::load_or_default().context("Failed to load settings.")?;

// These settings can be overridden by command-line arguments
if let Some(opt) = opts.debug_model {
settings.debug_model = opt;
if opts.debug_model {
settings.debug_model = true;
}
if opts.overwrite {
settings.overwrite = true;
Expand Down Expand Up @@ -186,13 +175,8 @@ pub fn handle_run_command(
}

/// Handle the `validate` command.
pub fn handle_validate_command(model_path: &Path, settings: Option<Settings>) -> Result<()> {
// Load program settings, if not provided
let settings = if let Some(settings) = settings {
settings
} else {
Settings::load().context("Failed to load settings.")?
};
pub fn handle_validate_command(model_path: &Path) -> Result<()> {
let settings = Settings::load_or_default().context("Failed to load settings.")?;

// Initialise program logger (we won't save log files when running the validate command)
log::init(&settings.log_level, None).context("Failed to initialise logging.")?;
Expand All @@ -205,17 +189,8 @@ pub fn handle_validate_command(model_path: &Path, settings: Option<Settings>) ->
}

/// Handle the `save-graphs` command.
pub fn handle_save_graphs_command(
model_path: &Path,
opts: &GraphOpts,
settings: Option<Settings>,
) -> Result<()> {
// Load program settings, if not provided
let mut settings = if let Some(settings) = settings {
settings
} else {
Settings::load().context("Failed to load settings.")?
};
pub fn handle_save_graphs_command(model_path: &Path, opts: &GraphOpts) -> Result<()> {
let mut settings = Settings::load_or_default().context("Failed to load settings.")?;

if opts.overwrite {
settings.overwrite = true;
Expand Down
35 changes: 3 additions & 32 deletions src/cli/example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use super::{RunOpts, handle_run_command};
use crate::example::patches::{get_patch_names, get_patches};
use crate::example::{Example, get_example_names};
use crate::patch::ModelPatch;
use crate::settings::Settings;
use anyhow::{Context, Result};
use clap::Subcommand;
use std::fs;
Expand Down Expand Up @@ -59,7 +58,7 @@ impl ExampleSubcommands {
new_path,
} => handle_example_extract_command(&name, new_path.as_deref(), patch)?,
Self::Run { name, patch, opts } => {
handle_example_run_command(&name, patch, &opts, None)?;
handle_example_run_command(&name, patch, &opts)?;
}
}

Expand Down Expand Up @@ -127,37 +126,9 @@ fn extract_example(name: &str, patch: bool, dest: &Path) -> Result<()> {
}

/// Handle the `example run` command.
pub fn handle_example_run_command(
name: &str,
patch: bool,
opts: &RunOpts,
settings: Option<Settings>,
) -> Result<()> {
pub fn handle_example_run_command(name: &str, patch: bool, opts: &RunOpts) -> Result<()> {
let temp_dir = TempDir::new().context("Failed to create temporary directory")?;
let model_path = temp_dir.path().join(name);
extract_example(name, patch, &model_path)?;
handle_run_command(&model_path, opts, settings)
}

#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;

fn assert_dir_non_empty(path: &Path) {
assert!(
path.read_dir().unwrap().next().is_some(),
"Directory is empty"
);
}

#[rstest]
#[case("muse1_default", false)]
#[case("simple_divisible", true)]
fn check_extract_example(#[case] name: &str, #[case] patch: bool) {
let tmp = TempDir::new().unwrap();
let dest = tmp.path().join("out");
extract_example(name, patch, &dest).unwrap();
assert_dir_non_empty(&dest);
}
handle_run_command(&model_path, opts)
}
27 changes: 17 additions & 10 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::log::DEFAULT_LOG_LEVEL;
use anyhow::Result;
use documented::DocumentedFields;
use serde::{Deserialize, Serialize};
use std::env;
use std::fmt::Write;
use std::path::{Path, PathBuf};

Expand Down Expand Up @@ -66,19 +67,25 @@ impl Default for Settings {
}

impl Settings {
/// Read the contents of a settings file from the model directory.
/// Read the contents of a settings file from the global MUSE2 configuration directory.
///
/// If the file is not present, default settings will be used.
/// If the file is not present or the user has set the `MUSE2_USE_DEFAULT_SETTINGS` environment
/// variable to 1, then the default settings will be used.
///
/// # Returns
///
/// The program settings as a `Settings` struct or an error if loading fails.
pub fn load() -> Result<Settings> {
Self::load_from_path(&get_settings_file_path())
pub fn load_or_default() -> Result<Settings> {
if env::var("MUSE2_USE_DEFAULT_SETTINGS").is_ok_and(|v| v == "1") {
Ok(Settings::default())
} else {
Self::from_path_or_default(&get_settings_file_path())
}
}
Comment on lines +78 to 84
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new MUSE2_USE_DEFAULT_SETTINGS environment variable functionality is not covered by unit tests. Consider adding a test case to verify that when this environment variable is set to "1", the function returns default settings without attempting to read from the file system, even if a settings file exists.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In principle, I'm not against this, but I don't want the test to depend on the state of the user's file system and I'm not sure of a good way round that.


/// Read from the specified path, returning
fn load_from_path(file_path: &Path) -> Result<Settings> {
/// Try to read settings from the specified path, returning `Settings::default()` if it doesn't
/// exist
fn from_path_or_default(file_path: &Path) -> Result<Settings> {
if !file_path.is_file() {
return Ok(Settings::default());
}
Expand Down Expand Up @@ -124,17 +131,17 @@ mod tests {
use tempfile::tempdir;

#[test]
fn settings_load_from_path_no_file() {
fn settings_from_path_or_default_no_file() {
let dir = tempdir().unwrap();
let file_path = dir.path().join(SETTINGS_FILE_NAME); // NB: doesn't exist
assert_eq!(
Settings::load_from_path(&file_path).unwrap(),
Settings::from_path_or_default(&file_path).unwrap(),
Settings::default()
);
}

#[test]
fn settings_load_from_path() {
fn settings_from_path_or_default() {
let dir = tempdir().unwrap();
let file_path = dir.path().join(SETTINGS_FILE_NAME);

Expand All @@ -144,7 +151,7 @@ mod tests {
}

assert_eq!(
Settings::load_from_path(&file_path).unwrap(),
Settings::from_path_or_default(&file_path).unwrap(),
Settings {
log_level: "warn".to_string(),
..Settings::default()
Expand Down
Loading