use anyhow::{Result, bail};
use clap::{Parser, Subcommand};
use esp_generate::template::{GeneratorOption, GeneratorOptionItem, Template};
use esp_generate::{
append_list_as_sentence,
config::{ActiveConfiguration, Relationships},
};
use esp_generate::{
cargo,
config::{find_option, flatten_options},
};
use esp_metadata::Chip;
use indexmap::IndexMap;
use inquire::{Select, Text};
use ratatui::crossterm::event;
use std::collections::HashSet;
use std::{
collections::HashMap,
env, fs,
path::{Path, PathBuf},
process::Command,
sync::LazyLock,
time::Duration,
};
use strum::IntoEnumIterator;
use taplo::formatter::Options;
use crate::template_files::TEMPLATE_FILES;
mod check;
mod module_selector;
mod template_files;
mod toolchain;
mod tui;
static TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
serde_yaml::from_str(
template_files::TEMPLATE_FILES
.iter()
.find_map(|(k, v)| if *k == "template.yaml" { Some(v) } else { None })
.unwrap(),
)
.unwrap()
});
#[derive(Parser, Debug)]
#[command(author, version, about = about(), long_about = None, subcommand_negates_reqs = true)]
struct Args {
name: Option<String>,
#[arg(short, long)]
chip: Option<Chip>,
#[arg(long)]
headless: bool,
#[arg(short, long, help = {
let mut all_options = Vec::new();
for option in TEMPLATE.options.iter() {
for opt in option.options() {
// Remove duplicates, which usually are chip-specific variations of an option.
// An example of this is probe-rs.
if !all_options.contains(&opt) && opt != "PLACEHOLDER" {
all_options.push(opt);
}
}
}
format!("Generation options: {} - For more information regarding the different options check the esp-generate README.md (https://github.com/esp-rs/esp-generate/blob/main/README.md).",all_options.join(", "))
})]
option: Vec<String>,
#[arg(short = 'O', long)]
output_path: Option<PathBuf>,
#[arg(short, long, global = true, action)]
#[cfg(feature = "update-informer")]
skip_update_check: bool,
#[arg(long)]
toolchain: Option<String>,
#[clap(subcommand)]
subcommands: Option<SubCommands>,
}
#[derive(Subcommand, Debug)]
enum SubCommands {
ListOptions,
Explain { option: String },
}
impl SubCommands {
fn handle(&self) -> Result<()> {
fn chip_info_text(options: &[&GeneratorOption], opt: &GeneratorOption) -> String {
let mut chips = Vec::new();
for option in options.iter().filter(|o| o.name == opt.name) {
chips.extend_from_slice(&option.chips);
}
let chip_count = Chip::iter().count();
if chips.is_empty() || chips.len() == chip_count {
String::new()
} else if chips.len() < chip_count / 2 {
format!(
"Only available on {}.",
chips
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)
} else {
format!(
"Not available on {}.",
Chip::iter()
.filter(|c| !chips.contains(c))
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
}
match self {
SubCommands::ListOptions => {
println!(
"The following template options are available. The group names are not part of the option name. Only one option in a group can be selected."
);
let mut groups = IndexMap::new();
let mut seen = HashSet::new();
let all_options = TEMPLATE.all_options();
for (index, option) in all_options
.iter()
.enumerate()
.filter(|(_, o)| !["toolchain", "module"].contains(&o.selection_group.as_str()))
{
let group = groups.entry(&option.selection_group).or_insert(Vec::new());
if seen.insert(&option.name) {
group.push(index);
}
}
for (group, options) in groups {
println!("Group: {}", group);
for option in options {
let option = &all_options[option];
let mut help_text = option.display_name.clone();
if !option.requires.is_empty() {
help_text.push_str(" Requires: ");
let readable = option.requires.iter().map(|option| {
if let Some(stripped) = option.strip_prefix('!') {
format!("{} unselected", stripped)
} else {
option.to_string()
}
});
help_text.push_str(&readable.collect::<Vec<String>>().join(", "));
help_text.push('.');
}
let chip_info = chip_info_text(&all_options, option);
if !chip_info.is_empty() {
help_text.push(' ');
help_text.push_str(&chip_info);
}
println!(" {}: {help_text}", option.name);
}
}
Ok(())
}
SubCommands::Explain { option } => {
let all_options = TEMPLATE.all_options();
if let Some(option) = all_options.iter().find(|o| &o.name == option) {
println!(
"Option: {}\n\n{}{}",
option.name,
option.display_name,
if option.help.is_empty() {
String::new()
} else {
format!("\n{}\n", option.help)
}
);
if !option.requires.is_empty() {
println!();
let positive_req = option.requires.iter().filter(|r| !r.starts_with("!"));
let negative_req = option.requires.iter().filter(|r| r.starts_with("!"));
if positive_req.clone().next().is_some() {
println!("Requires the following options to be set:");
for require in positive_req {
println!(" {}", require);
}
}
if negative_req.clone().next().is_some() {
println!("Requires the following options to NOT be set:");
for require in negative_req {
if let Some(stripped) = require.strip_prefix('!') {
println!(" {}", stripped);
}
}
}
}
let chip_info = chip_info_text(&all_options, option);
if !chip_info.is_empty() {
println!("{}", chip_info);
}
} else {
println!("Unknown option: {}", option);
}
Ok(())
}
}
}
}
#[cfg(feature = "update-informer")]
fn check_for_update(name: &str, version: &str) {
use update_informer::{Check, registry};
let informer =
update_informer::new(registry::Crates, name, version).interval(Duration::from_secs(0));
if let Some(version) = informer.check_version().ok().flatten() {
log::warn!("🚀 A new version of {name} is available: {version}");
}
}
fn about() -> String {
let mut about = String::from(
"Template generation tool to create no_std applications targeting Espressif's chips.\n\nThe template will use these versions:\n",
);
let toml = cargo::CargoToml::load(
TEMPLATE_FILES
.iter()
.find(|(k, _)| *k == "Cargo.toml")
.expect("Cargo.toml not found in template")
.1,
)
.expect("Failed to read Cargo.toml");
toml.visit_dependencies(|_, name, table| {
if name == "dependencies" {
for entry in table.iter() {
let name = entry.0;
if name.starts_with("esp-") {
about.push_str(&format!("{:23 } {}\n", name, toml.dependency_version(name)));
}
}
}
});
about
}
fn setup_args_interactive(args: &mut Args) -> Result<()> {
if args.headless {
let mut missing = String::from(
"You are in headless mode, but esp-generate needs more information to generate your project.",
);
if args.chip.is_none() {
missing.push_str(
"\nThe target chip is missing. Add --chip <your-chip-name> to the command.",
);
}
if args.name.is_none() {
missing.push_str("\nThe project name is missing. Add the name of your project to the end of the command.");
}
bail!("{missing}");
}
if args.chip.is_none() {
let chip_variants = Chip::iter().collect::<Vec<_>>();
let chip = Select::new("Select your target chip:", chip_variants).prompt()?;
args.chip = Some(chip);
}
if args.name.is_none() {
let project_name = Text::new("Enter project name:")
.with_default("my-esp-project")
.prompt()?;
args.name = Some(project_name);
}
Ok(())
}
fn main() -> Result<()> {
tui::setup_logger().expect("logger should only be initialized once");
let mut args = Args::parse();
if let Some(subcommand) = args.subcommands {
return subcommand.handle();
}
#[cfg(feature = "update-informer")]
if !args.skip_update_check {
check_for_update(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
}
if args.chip.is_none() || args.name.is_none() {
setup_args_interactive(&mut args)?;
}
let chip = args.chip.unwrap();
let name = args.name.clone().unwrap();
let path = &args
.output_path
.clone()
.unwrap_or_else(|| env::current_dir().unwrap());
if !path.is_dir() {
bail!("Output path must be a directory");
}
if path.join(&name).exists() {
bail!("Directory already exists");
}
let versions = cargo::CargoToml::load(
TEMPLATE_FILES
.iter()
.find(|(k, _)| *k == "Cargo.toml")
.expect("Cargo.toml not found in template")
.1,
)
.expect("Failed to read Cargo.toml");
let esp_hal_version = versions.dependency_version("esp-hal");
let esp_hal_version_full = if let Some(stripped) = esp_hal_version.strip_prefix("~") {
let mut processed = stripped.to_string();
while processed.chars().filter(|c| *c == '.').count() < 2 {
processed.push_str(".0");
}
processed
} else {
esp_hal_version.clone()
};
let msrv: check::Version = versions.msrv().parse().unwrap();
let mut toolchain_scan = if args.headless {
None
} else {
Some(toolchain::start_toolchain_scan(
chip,
args.toolchain.clone(),
msrv.clone(),
))
};
let mut template = TEMPLATE.clone();
remove_incompatible_chip_options(chip, &mut template.options);
module_selector::populate_module_category(chip, &mut template.options);
process_options(&template, &args)?;
let mut initial_selected = args.option.clone();
if let Some(ref tc) = args.toolchain {
initial_selected.push(tc.clone());
}
let mut selected = if !args.headless {
let repository = tui::Repository::new(chip, template.options.clone(), &initial_selected);
let mut app = tui::App::new(repository);
let mut terminal = tui::init_terminal()?;
let mut final_selected: Option<Vec<String>> = None;
let mut running = true;
let mut toolchains_populated = false;
while running {
if let Some(scan) = toolchain_scan.as_mut() {
match scan.try_get_toolchain_list() {
None => {
app.set_toolchains_loading(true);
}
Some(Ok(list)) => {
if !toolchains_populated {
toolchain::populate_toolchain_category_from_list(
&mut template.options,
&mut Vec::new(),
list,
)?;
toolchain::populate_toolchain_category_from_list(
&mut app.repository.config.options,
&mut app.repository.config.flat_options,
list,
)?;
toolchains_populated = true;
}
app.set_toolchains_loading(false);
}
Some(Err(err)) => {
log::warn!("Toolchain scan failed: {err}");
app.set_toolchains_loading(false);
toolchains_populated = true;
}
}
}
app.draw(&mut terminal)?;
if event::poll(Duration::from_millis(100))? {
match app.handle_event(event::read()?)? {
tui::AppResult::Continue => {}
tui::AppResult::Quit => {
final_selected = None;
running = false;
}
tui::AppResult::Save => {
final_selected = Some(app.selected_options());
running = false;
}
}
}
}
tui::restore_terminal()?;
let Some(selected) = final_selected else {
return Ok(());
};
selected
} else {
initial_selected
};
let flat_options = flatten_options(&template.options);
let mut toolchain_replaced = false;
let selected_options = format!(
"--chip {}{}",
chip,
selected.iter().fold(String::new(), |mut acc, s| {
if Some(s) == args.toolchain.as_ref() && !toolchain_replaced {
acc.push_str(" --toolchain ");
toolchain_replaced = true;
} else {
acc.push_str(" -o ");
};
acc.push_str(s);
acc
})
);
if !args.headless {
println!("Selected options: {selected_options}");
}
let selected_toolchain = if args.headless {
args.toolchain.clone()
} else {
selected.iter().find_map(|name| {
let (_, opt) = find_option(name, &flat_options, chip)?;
if opt.selection_group == "toolchain" {
Some(name.clone())
} else {
None
}
})
};
let selected_module = selected.iter().find_map(|name| {
let (_, opt) = find_option(name, &flat_options, chip)?;
if opt.selection_group == "module" {
Some(name.clone())
} else {
None
}
});
for idx in 0..selected.len() {
let (_, option) = find_option(&selected[idx], &flat_options, chip).unwrap();
selected.push(option.selection_group.clone());
}
selected.push(chip.to_string());
selected.push(if chip.is_riscv() {
"riscv".to_string()
} else {
"xtensa".to_string()
});
if selected_toolchain.is_some() {
selected.push("toolchain-selected".to_string());
}
let wokwi_devkit = match chip {
Chip::Esp32 => "board-esp32-devkit-c-v4",
Chip::Esp32c2 => "",
Chip::Esp32c3 => "board-esp32-c3-devkitm-1",
Chip::Esp32c5 => "board-esp32-c5-devkitc-1",
Chip::Esp32c6 => "board-esp32-c6-devkitc-1",
Chip::Esp32c61 => "board-esp32-c61-devkitc-1",
Chip::Esp32h2 => "board-esp32-h2-devkitm-1",
Chip::Esp32s2 => "board-esp32-s2-devkitm-1",
Chip::Esp32s3 => "board-esp32-s3-devkitc-1",
};
let max_dram2 = match chip {
Chip::Esp32 => 98768,
Chip::Esp32c2 => 66416, Chip::Esp32c3 => 66320, Chip::Esp32c5 => 65536, Chip::Esp32c6 => 65536, Chip::Esp32c61 => 65536, Chip::Esp32h2 => 69392, Chip::Esp32s2 => 139264,
Chip::Esp32s3 => 73744, };
let mut variables = vec![
("project-name".to_string(), name.clone()),
("mcu".to_string(), chip.to_string()),
("wokwi-board".to_string(), wokwi_devkit.to_string()),
(
"generate-version".to_string(),
env!("CARGO_PKG_VERSION").to_string(),
),
("generate-parameters".to_string(), selected_options),
("esp-hal-version-full".to_string(), esp_hal_version_full),
("max-dram2-uninit".to_string(), format!("{max_dram2}")),
];
variables.push(("rust_target".to_string(), chip.target().to_string()));
if let Some(tc) = selected_toolchain.as_ref() {
variables.push(("rust_toolchain".to_string(), tc.clone()));
}
if let Some(ref module_name) = selected_module {
if let Some(module) = esp_generate::modules::find_module(module_name) {
if !module.reserved_gpios.is_empty() {
selected.push("module-selected".to_string());
if module.octal_psram {
selected.push("octal-psram".to_string());
}
let reserved_gpio_code = module
.reserved_gpios
.iter()
.map(|g| format!(" let _ = peripherals.GPIO{g};"))
.collect::<Vec<_>>()
.join("\n");
variables.push(("reserved_gpio_code".to_string(), reserved_gpio_code));
}
}
}
let project_dir = path.join(&name);
fs::create_dir(&project_dir)?;
for &(file_path, contents) in template_files::TEMPLATE_FILES.iter() {
let mut file_path = file_path.to_string();
if let Some(processed) = process_file(contents, &selected, &variables, &mut file_path) {
let file_path = project_dir.join(file_path);
fs::create_dir_all(file_path.parent().unwrap())?;
fs::write(file_path, processed)?;
}
}
Command::new("cargo")
.args([
"fmt",
"--",
"--config",
"group_imports=StdExternalCrate",
"--config",
"imports_granularity=Module",
])
.current_dir(&project_dir)
.output()?;
let input = fs::read_to_string(project_dir.join("Cargo.toml"))?;
let format_options = Options {
align_entries: true,
reorder_keys: true,
reorder_arrays: true,
..Default::default()
};
let formated = taplo::formatter::format(&input, format_options);
fs::write(project_dir.join("Cargo.toml"), formated)?;
if should_initialize_git_repo(&project_dir) {
Command::new("git")
.arg("init")
.current_dir(&project_dir)
.output()?;
} else {
log::warn!("Current directory is already in a git repository, skipping git initialization");
}
check::check(
&project_dir,
chip,
selected.contains(&"probe-rs".to_string()),
msrv,
selected.contains(&"stack-smashing-protection".to_string())
&& selected.contains(&"riscv".to_string()),
args.headless,
selected_toolchain.as_deref(),
);
Ok(())
}
fn remove_incompatible_chip_options(chip: Chip, options: &mut Vec<GeneratorOptionItem>) {
options.retain_mut(|opt| match opt {
GeneratorOptionItem::Category(category) => {
remove_incompatible_chip_options(chip, &mut category.options);
!category.options.is_empty()
}
GeneratorOptionItem::Option(option) => {
option.chips.is_empty() || option.chips.contains(&chip)
}
});
}
#[derive(Clone, Copy)]
enum BlockKind {
Root,
IfElse(bool, bool),
}
impl BlockKind {
fn include_line(self) -> bool {
match self {
BlockKind::Root => true,
BlockKind::IfElse(current, any) => current && !any,
}
}
fn new_if(current: bool) -> BlockKind {
BlockKind::IfElse(current, false)
}
fn into_else_if(self, condition: bool) -> BlockKind {
let BlockKind::IfElse(previous, any) = self else {
panic!("ELIF without IF");
};
BlockKind::IfElse(condition, any || previous)
}
fn into_else(self) -> BlockKind {
let BlockKind::IfElse(previous, any) = self else {
panic!("ELSE without IF");
};
BlockKind::IfElse(!any, any || previous)
}
}
fn process_file(
contents: &str, options: &[String], variables: &[(String, String)], file_path: &mut String, ) -> Option<String> {
let mut res = String::new();
let mut replace: Option<Vec<(String, String)>> = None;
let mut include = vec![BlockKind::Root];
let mut file_directives = true;
let mut engine = somni_expr::Context::new();
engine.add_function("option", move |cond: &str| -> bool {
options.iter().any(|c| c == cond)
});
let mut include_file = true;
for (line_no, line) in contents.lines().enumerate() {
let line_no = line_no + 1;
let trimmed: &str = line.trim();
if file_directives {
if let Some(cond) = trimmed
.strip_prefix("//INCLUDEFILE ")
.or_else(|| trimmed.strip_prefix("#INCLUDEFILE "))
.or_else(|| trimmed.strip_prefix("--INCLUDEFILE "))
{
include_file = engine.evaluate::<bool>(cond).unwrap();
continue;
} else if let Some(include_as) = trimmed
.strip_prefix("//INCLUDE_AS ")
.or_else(|| trimmed.strip_prefix("#INCLUDE_AS "))
.or_else(|| trimmed.strip_prefix("--INCLUDE_AS "))
{
*file_path = include_as.trim().to_string();
continue;
}
}
if !include_file {
return None;
}
file_directives = false;
if trimmed == "#[rustfmt::skip]" {
log::info!("Skipping rustfmt");
continue;
}
if let Some(what) = trimmed
.strip_prefix("#REPLACE ")
.or_else(|| trimmed.strip_prefix("//REPLACE "))
.or_else(|| trimmed.strip_prefix("--REPLACE "))
{
let replacements = what
.split(" && ")
.filter_map(|pair| {
let mut parts = pair.split_whitespace();
if let (Some(pattern), Some(var_name)) = (parts.next(), parts.next()) {
if let Some((_, value)) = variables.iter().find(|(key, _)| key == var_name)
{
Some((pattern.to_string(), value.clone()))
} else {
None
}
} else {
None
}
})
.collect::<Vec<_>>();
if !replacements.is_empty() {
replace = Some(replacements);
}
} else if trimmed.starts_with("#IF ")
|| trimmed.starts_with("//IF ")
|| trimmed.starts_with("--IF ")
{
let cond = trimmed
.strip_prefix("#IF ")
.or_else(|| trimmed.strip_prefix("//IF "))
.or_else(|| trimmed.strip_prefix("--IF "))
.unwrap();
let last = *include.last().unwrap();
let current = if last.include_line() {
engine.evaluate::<bool>(cond).unwrap()
} else {
false
};
include.push(BlockKind::new_if(current));
} else if trimmed.starts_with("#ELIF ")
|| trimmed.starts_with("//ELIF ")
|| trimmed.starts_with("--ELIF ")
{
let cond = trimmed
.strip_prefix("#ELIF ")
.or_else(|| trimmed.strip_prefix("//ELIF "))
.or_else(|| trimmed.strip_prefix("--ELIF "))
.unwrap();
let last = include.pop().unwrap();
let current = if matches!(last, BlockKind::IfElse(false, false)) {
engine.evaluate::<bool>(cond).unwrap()
} else {
false
};
include.push(last.into_else_if(current));
} else if trimmed.starts_with("#ELSE")
|| trimmed.starts_with("//ELSE")
|| trimmed.starts_with("--ELSE")
{
let last = include.pop().unwrap();
include.push(last.into_else());
} else if trimmed.starts_with("#ENDIF")
|| trimmed.starts_with("//ENDIF")
|| trimmed.starts_with("--ENDIF")
{
let prev = include.pop();
assert!(
matches!(prev, Some(BlockKind::IfElse(_, _))),
"ENDIF without IF in {file_path}:{line_no}"
);
} else if include.iter().all(|v| v.include_line()) {
let mut line = line.to_string();
if trimmed.starts_with("#+") {
line = line.replace("#+", "");
}
if trimmed.starts_with("//+") {
line = line.replace("//+", "");
}
if trimmed.starts_with("--+") {
line = line.replace("--+", "");
}
if let Some(replacements) = &replace {
for (pattern, value) in replacements {
line = line.replace(pattern, value);
}
}
res.push_str(&line);
res.push('\n');
replace = None;
}
}
Some(res)
}
fn process_options(template: &Template, args: &Args) -> Result<()> {
let mut success = true;
let all_options = template.all_options();
let arg_chip = args.chip.unwrap();
let flat_options = flatten_options(&template.options);
let selected_config = ActiveConfiguration {
chip: arg_chip,
selected: args
.option
.iter()
.flat_map(|opt_name| flat_options.iter().position(|o| &o.name == opt_name))
.collect(),
flat_options,
options: template.options.clone(),
};
let mut same_selection_group: HashMap<&str, Vec<&str>> = HashMap::new();
for option in &selected_config.selected {
let option = selected_config.flat_options[*option].name.as_str();
let mut option_found = false;
let mut option_found_for_chip = false;
for &option_item in all_options.iter().filter(|item| item.name == option) {
option_found = true;
if !option_item.chips.contains(&arg_chip) && !option_item.chips.is_empty() {
continue;
}
option_found_for_chip = true;
if selected_config.is_option_active(option_item) {
if !option_item.selection_group.is_empty() {
let options = same_selection_group
.entry(&option_item.selection_group)
.or_default();
if !options.contains(&option) {
options.push(option);
}
}
continue;
}
success = false;
let o = GeneratorOptionItem::Option(option_item.clone());
let Relationships {
requires,
disabled_by,
..
} = selected_config.collect_relationships(&o);
if !requires
.iter()
.all(|requirement| args.option.iter().any(|r| r == requirement))
{
log::error!(
"Option '{}' requires {}",
option_item.name,
option_item.requires.join(", ")
);
}
for disabled in disabled_by {
log::error!("Option '{}' is disabled by {}", option_item.name, disabled);
}
}
if !option_found {
log::error!("Unknown option '{option}'");
success = false;
} else if !option_found_for_chip {
log::error!("Option '{option}' is not supported for chip {arg_chip}");
success = false;
}
}
for (_group, entries) in same_selection_group {
if entries.len() > 1 {
log::error!(
"{}",
append_list_as_sentence(
"The following options can not be enabled together:",
"",
&entries
)
);
success = false;
}
}
if !success {
bail!("Invalid options provided");
} else {
Ok(())
}
}
fn should_initialize_git_repo(mut path: &Path) -> bool {
loop {
let dotgit_path = path.join(".git");
if dotgit_path.exists() && dotgit_path.is_dir() {
return false;
}
if let Some(parent) = path.parent() {
path = parent;
} else {
break;
}
}
true
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_nested_if_else1() {
let res = process_file(
r#"
#IF option("opt1")
opt1
#IF option("opt2")
opt2
#ELSE
!opt2
#ENDIF
#ELSE
!opt1
#ENDIF
"#,
&["opt1".to_string(), "opt2".to_string()],
&[],
&mut String::from("main.rs"),
)
.unwrap();
assert_eq!(
r#"
opt1
opt2
"#
.trim(),
res.trim()
);
}
#[test]
fn test_nested_if_else2() {
let res = process_file(
r#"
#IF option("opt1")
opt1
#IF option("opt2")
opt2
#ELSE
!opt2
#ENDIF
#ELSE
!opt1
#ENDIF
"#,
&[],
&[],
&mut String::from("main.rs"),
)
.unwrap();
assert_eq!(
r#"
!opt1
"#
.trim(),
res.trim()
);
}
#[test]
fn test_nested_if_else3() {
let res = process_file(
r#"
#IF option("opt1")
opt1
#IF option("opt2")
opt2
#ELSE
!opt2
#ENDIF
#ELSE
!opt1
#ENDIF
"#,
&["opt1".to_string()],
&[],
&mut String::from("main.rs"),
)
.unwrap();
assert_eq!(
r#"
opt1
!opt2
"#
.trim(),
res.trim()
);
}
#[test]
fn test_nested_if_else4() {
let res = process_file(
r#"
#IF option("opt1")
#IF option("opt2")
opt2
#ELSE
!opt2
#ENDIF
opt1
#ENDIF
"#,
&["opt1".to_string()],
&[],
&mut String::from("main.rs"),
)
.unwrap();
assert_eq!(
r#"
!opt2
opt1
"#
.trim(),
res.trim()
);
}
#[test]
fn test_nested_if_else5() {
let res = process_file(
r#"
#IF option("opt1")
#IF option("opt2")
opt2
#ELSE
!opt2
#ENDIF
opt1
#ENDIF
"#,
&["opt2".to_string()],
&[],
&mut String::from("main.rs"),
)
.unwrap();
assert_eq!(
r#"
"#
.trim(),
res.trim()
);
}
#[test]
fn test_basic_elseif() {
let template = r#"
#IF option("opt1")
opt1
#ELIF option("opt2")
opt2
#ELIF option("opt3")
opt3
#ELSE
opt4
#ENDIF
"#;
const PAIRS: &[(&[&str], &str)] = &[
(&["opt1"], "opt1"),
(&["opt1", "opt2"], "opt1"),
(&["opt1", "opt3"], "opt1"),
(&["opt1", "opt2", "opt3"], "opt1"),
(&["opt2"], "opt2"),
(&["opt2", "opt3"], "opt2"),
(&["opt3"], "opt3"),
(&["opt4"], "opt4"),
(&[], "opt4"),
];
for (options, expected) in PAIRS.iter().cloned() {
let res = process_file(
template,
&options.iter().map(|o| o.to_string()).collect::<Vec<_>>(),
&[],
&mut String::from("main.rs"),
)
.unwrap();
assert_eq!(expected, res.trim(), "options: {:?}", options);
}
}
}