mirror of
https://github.com/edera-dev/sprout.git
synced 2026-11-04 17:42:20 +00:00
feat(sprout): generator variants (#105)
Generators can now declare variant axes. Every generated entry is multiplied by each combination of choices, one per axis. A choice sets the axis name to the choice name and inserts its own values, and the entry name is suffixed with the choice names. Generators can also declare exclude rules, which remove generated entries whose values match every glob pattern in a rule. This covers unwanted variant combinations and entries like BLS rescue kernels. Entry matching for default-entry and the bootloader interface now accepts `*` anywhere in the pattern instead of only at the end, so generated entry names can be matched regardless of the kernel version in the middle of the name. The BLS generator gains a pin-names option (default true, the existing behavior). Two BLS generators reading the same directory previously produced entries with identical names; disabling pin-names prefixes them with the generator name instead. Sprout now warns when more than one entry has the same name. * docs(config): note that variants suffix pinned BLS entry names Pinned BLS entry names match the BLS entry ids, but variants still append their choice names, so those entries need a pattern like <id>-* to be selected.
This commit is contained in:
9 files changed
+419
-26
No files matched your search
@@ -81,6 +81,7 @@ pub fn scan(
|
||||
..Default::default()
|
||||
},
|
||||
path: format!("{}\\loader", root),
|
||||
pin_names: true,
|
||||
};
|
||||
|
||||
// Generate a unique name for the BLS generator and insert the generator into the configuration.
|
||||
|
||||
+22
-10
@@ -2,6 +2,7 @@ use crate::context::SproutContext;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::{String, ToString};
|
||||
use edera_sprout_config::entries::EntryDeclaration;
|
||||
use edera_sprout_parsing::glob_match;
|
||||
|
||||
/// Represents an entry that is stamped and ready to be booted.
|
||||
#[derive(Clone)]
|
||||
@@ -105,18 +106,29 @@ impl BootableEntry {
|
||||
}
|
||||
|
||||
/// Determine if this entry matches `needle` by comparing to the name or title of the entry.
|
||||
/// If `needle` ends with *, we will match a partial match.
|
||||
/// The `needle` is a glob pattern, where `*` matches any sequence of characters.
|
||||
/// A `needle` without any `*` must equal the name or title exactly.
|
||||
pub fn is_match(&self, needle: &str) -> bool {
|
||||
// If the needle ends with '*', we will accept a partial match.
|
||||
if needle.ends_with("*") {
|
||||
// Strip off any '*' at the end.
|
||||
let partial = needle.trim_end_matches("*");
|
||||
// Check if the name or title start with the partial match.
|
||||
return self.name.starts_with(partial) || self.title.starts_with(partial);
|
||||
}
|
||||
glob_match(needle, &self.name) || glob_match(needle, &self.title)
|
||||
}
|
||||
|
||||
// Standard quality matching rules.
|
||||
self.name == needle || self.title == needle
|
||||
/// Create a variant of this entry, with the name suffixed by `suffix` and using `context`.
|
||||
/// The actions of the entry are stamped with `context`, the same as the list generator.
|
||||
/// Variants of the same entry keep the sort key of this entry, so they stay grouped
|
||||
/// together in the order they were created in.
|
||||
pub fn variant(&self, suffix: &str, context: Rc<SproutContext>) -> Self {
|
||||
let mut entry = self.clone();
|
||||
entry.name.push_str(suffix);
|
||||
entry.declaration.actions = context
|
||||
.stamp_iter(entry.declaration.actions.iter())
|
||||
.collect();
|
||||
// Without any sort key, the name is used to sort, which differs between variants.
|
||||
// Pin the sort key to the name of this entry to keep the variants together.
|
||||
if entry.sort_key.is_none() && entry.declaration.sort_key.is_none() {
|
||||
entry.sort_key = Some(self.name.clone());
|
||||
}
|
||||
entry.context = context;
|
||||
entry
|
||||
}
|
||||
|
||||
/// Set the sort key of the entry. This is used to sort entries via version comparison.
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use crate::context::SproutContext;
|
||||
use crate::entries::BootableEntry;
|
||||
use alloc::collections::BTreeSet;
|
||||
use alloc::format;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use edera_sprout_config::generators::GeneratorDeclaration;
|
||||
use edera_sprout_parsing::{build_variants, rule_matches};
|
||||
|
||||
/// The BLS generator.
|
||||
pub mod bls;
|
||||
@@ -15,10 +19,8 @@ pub mod list;
|
||||
/// The matrix generator.
|
||||
pub mod matrix;
|
||||
|
||||
/// Runs the generator specified by the `generator` option.
|
||||
/// It uses the specified `context` as the parent context for
|
||||
/// the generated entries, injecting more values if needed.
|
||||
pub fn generate(
|
||||
/// Runs the generator kind specified by the `generator` option.
|
||||
fn generate_base(
|
||||
context: Rc<SproutContext>,
|
||||
generator: &GeneratorDeclaration,
|
||||
) -> Result<Vec<BootableEntry>> {
|
||||
@@ -32,3 +34,92 @@ pub fn generate(
|
||||
bail!("unknown generator configuration");
|
||||
}
|
||||
}
|
||||
|
||||
/// Multiplies `entries` by every combination of the variants in `generator`.
|
||||
fn apply_variants(
|
||||
entries: Vec<BootableEntry>,
|
||||
generator: &GeneratorDeclaration,
|
||||
) -> Result<Vec<BootableEntry>> {
|
||||
// Without any variants, the entries are used as-is.
|
||||
if generator.variants.is_empty() {
|
||||
return Ok(entries);
|
||||
}
|
||||
|
||||
// Validate the axes, as an axis without choices would silently produce no entries,
|
||||
// and duplicate choice names would produce entries with the same name.
|
||||
let mut axes = Vec::new();
|
||||
for (axis, choices) in &generator.variants {
|
||||
if choices.is_empty() {
|
||||
bail!("variant axis '{}' has no choices", axis);
|
||||
}
|
||||
let mut seen = BTreeSet::new();
|
||||
for choice in choices {
|
||||
if !seen.insert(choice.name.as_str()) {
|
||||
bail!(
|
||||
"variant axis '{}' has more than one choice named '{}'",
|
||||
axis,
|
||||
choice.name
|
||||
);
|
||||
}
|
||||
}
|
||||
axes.push((
|
||||
axis.clone(),
|
||||
choices
|
||||
.iter()
|
||||
.map(|choice| (choice.name.clone(), choice.values.clone()))
|
||||
.collect(),
|
||||
));
|
||||
}
|
||||
|
||||
let combinations = build_variants(&axes);
|
||||
let mut result = Vec::with_capacity(entries.len() * combinations.len());
|
||||
for entry in &entries {
|
||||
for combination in &combinations {
|
||||
let mut context = entry.context().fork();
|
||||
context.insert(&combination.values);
|
||||
let suffix: String = combination
|
||||
.names
|
||||
.iter()
|
||||
.map(|name| format!("-{}", name))
|
||||
.collect();
|
||||
result.push(entry.variant(&suffix, context.freeze()));
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Runs the generator specified by the `generator` option, named `name`.
|
||||
/// It uses the specified `context` as the parent context for
|
||||
/// the generated entries, injecting more values if needed.
|
||||
/// Entry names are prefixed with the generator name unless pinned,
|
||||
/// then variants are applied, and then excluded entries are removed.
|
||||
pub fn generate(
|
||||
name: &str,
|
||||
context: Rc<SproutContext>,
|
||||
generator: &GeneratorDeclaration,
|
||||
) -> Result<Vec<BootableEntry>> {
|
||||
let mut entries = generate_base(context, generator)?;
|
||||
|
||||
// We will prefix all entries with [name]-, provided the name is not pinned.
|
||||
let prefix = format!("{}-", name);
|
||||
for entry in &mut entries {
|
||||
if !entry.is_pin_name() {
|
||||
entry.prepend_name_prefix(&prefix);
|
||||
}
|
||||
}
|
||||
|
||||
let mut entries = apply_variants(entries, generator)?;
|
||||
|
||||
// Remove any entries that match an exclusion rule.
|
||||
if !generator.exclude.is_empty() {
|
||||
entries.retain(|entry| {
|
||||
let values = entry.context().all_values();
|
||||
!generator
|
||||
.exclude
|
||||
.iter()
|
||||
.any(|rule| rule_matches(rule, &values))
|
||||
});
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
@@ -206,7 +206,9 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
|
||||
// Pin the entry name to prevent prefixing.
|
||||
// This is needed as the bootloader interface requires the name to be
|
||||
// the same as the entry file name, minus the .conf extension.
|
||||
boot.mark_pin_name();
|
||||
if bls.pin_names {
|
||||
boot.mark_pin_name();
|
||||
}
|
||||
|
||||
// Add the BLS entry to the list, along with the bootable entry.
|
||||
entries.push((entry, boot));
|
||||
|
||||
+18
-10
@@ -9,7 +9,12 @@ use crate::{
|
||||
options::SproutOptions,
|
||||
phases::phase,
|
||||
};
|
||||
use alloc::{collections::BTreeMap, format, string::ToString, vec::Vec};
|
||||
use alloc::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
format,
|
||||
string::ToString,
|
||||
vec::Vec,
|
||||
};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use core::{ops::Deref, time::Duration};
|
||||
use edera_sprout_bls::compare_versions;
|
||||
@@ -245,18 +250,21 @@ fn run() -> Result<()> {
|
||||
for (name, generator) in config.generators {
|
||||
let context = context.fork().freeze();
|
||||
|
||||
// We will prefix all entries with [name]-, provided the name is not pinned.
|
||||
let prefix = format!("{}-", name);
|
||||
|
||||
// Add all the entries generated by the generator to the entry list.
|
||||
// The generator specifies the context associated with the entry.
|
||||
for mut entry in generators::generate(context.clone(), &generator)? {
|
||||
// If the entry name is not pinned, prepend the name prefix.
|
||||
if !entry.is_pin_name() {
|
||||
entry.prepend_name_prefix(&prefix);
|
||||
}
|
||||
entries.extend(generators::generate(&name, context, &generator)?);
|
||||
}
|
||||
|
||||
entries.push(entry);
|
||||
// Entry names are used to select entries, so duplicates make the selection ambiguous.
|
||||
// This can happen when two generators pin names, like two BLS generators reading the
|
||||
// same BLS directory.
|
||||
let mut seen_names = BTreeSet::new();
|
||||
for entry in &entries {
|
||||
if !seen_names.insert(entry.name()) {
|
||||
warn!(
|
||||
"more than one entry is named {}, selecting it by name is ambiguous",
|
||||
entry.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use crate::generators::bls::BlsConfiguration;
|
||||
use crate::generators::list::ListConfiguration;
|
||||
use crate::generators::matrix::MatrixConfiguration;
|
||||
use crate::generators::variants::VariantChoice;
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Configuration for the BLS generator.
|
||||
@@ -12,6 +16,9 @@ pub mod list;
|
||||
/// Configuration for the matrix generator.
|
||||
pub mod matrix;
|
||||
|
||||
/// Configuration for generator variants.
|
||||
pub mod variants;
|
||||
|
||||
/// Declares a generator configuration.
|
||||
/// Generators allow generating entries at runtime based on a set of data.
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
@@ -37,4 +44,29 @@ pub struct GeneratorDeclaration {
|
||||
/// List generator configuration.
|
||||
/// Allows you to specify a list of values to generate an entry from.
|
||||
pub list: Option<ListConfiguration>,
|
||||
/// Variants to apply to every entry produced by the generator.
|
||||
/// Each key is an axis, and each axis has a list of choices. Every generated entry
|
||||
/// is multiplied by every combination of choices, one choice from each axis.
|
||||
/// For example,
|
||||
/// variants.console = [{ name = "serial" }, { name = "graphics" }]
|
||||
/// variants.mode = [{ name = "normal" }, { name = "debug", values.mode-options = "loglevel=7" }]
|
||||
/// would turn each generated entry into four entries:
|
||||
/// console = serial, mode = normal
|
||||
/// console = serial, mode = debug
|
||||
/// console = graphics, mode = normal
|
||||
/// console = graphics, mode = debug
|
||||
/// The axis name is set to the name of the chosen choice, and then the values of the
|
||||
/// chosen choice are inserted, so `$console` and `$mode-options` can be used by the entry.
|
||||
/// The entry name is suffixed with the choice names, like `-graphics-debug`.
|
||||
/// Axes are applied in alphabetical order. Choices are applied in the declared order.
|
||||
#[serde(default)]
|
||||
pub variants: BTreeMap<String, Vec<VariantChoice>>,
|
||||
/// Rules that exclude generated entries, applied after variants.
|
||||
/// Each rule is a map of value names to glob patterns, where `*` matches anything.
|
||||
/// An entry is excluded if every value named in any one rule matches its pattern.
|
||||
/// Values are matched before they are stamped. For example,
|
||||
/// exclude = [{ version = "0-rescue-*" }, { console = "graphics", mode = "debug" }]
|
||||
/// excludes BLS rescue entries and the combination of the graphics and debug variants.
|
||||
#[serde(default)]
|
||||
pub exclude: Vec<BTreeMap<String, String>>,
|
||||
}
|
||||
@@ -8,13 +8,37 @@ const BLS_TEMPLATE_PATH: &str = "\\loader";
|
||||
/// The configuration of the BLS generator.
|
||||
/// The BLS uses the Bootloader Specification to produce
|
||||
/// entries from an input template.
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct BlsConfiguration {
|
||||
/// The entry to use for as a template.
|
||||
pub entry: EntryDeclaration,
|
||||
/// The path to the BLS directory.
|
||||
#[serde(default = "default_bls_path")]
|
||||
pub path: String,
|
||||
/// Whether generated entries keep the name of the BLS entry file as-is.
|
||||
/// When enabled, the generator name is not prepended, so the entry names match
|
||||
/// the BLS entry ids used by tools like `bootctl set-default`. Variants still append
|
||||
/// their choice names, so entries of a generator with variants never match the ids
|
||||
/// exactly, and are best selected with a pattern like `<id>-*`. Two BLS generators
|
||||
/// that read the same BLS directory will produce entries with the same names, so
|
||||
/// generators that reuse a BLS directory should disable this.
|
||||
#[serde(default = "default_pin_names", rename = "pin-names")]
|
||||
pub pin_names: bool,
|
||||
}
|
||||
|
||||
/// The default configuration, which matches an empty BLS generator section.
|
||||
impl Default for BlsConfiguration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
entry: Default::default(),
|
||||
path: default_bls_path(),
|
||||
pin_names: default_pin_names(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_pin_names() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_bls_path() -> String {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::String;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A single choice of a variant axis.
|
||||
/// See [crate::generators::GeneratorDeclaration::variants] for how variants are applied.
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
pub struct VariantChoice {
|
||||
/// The name of the choice. This is set as the value of the axis name,
|
||||
/// and is appended to the name of each entry the choice applies to.
|
||||
pub name: String,
|
||||
/// The values to insert into the context of each entry the choice applies to.
|
||||
#[serde(default)]
|
||||
pub values: BTreeMap<String, String>,
|
||||
}
|
||||
@@ -172,6 +172,97 @@ pub fn initramfs_candidates(
|
||||
candidates.into_iter()
|
||||
}
|
||||
|
||||
/// Matches `text` against a glob `pattern` where `*` matches any sequence of characters,
|
||||
/// including an empty one. Every other character matches itself exactly.
|
||||
/// A pattern without any `*` only matches `text` when they are equal.
|
||||
pub fn glob_match(pattern: &str, text: &str) -> bool {
|
||||
let pattern = pattern.as_bytes();
|
||||
let text = text.as_bytes();
|
||||
|
||||
// The current position in the pattern and the text.
|
||||
let (mut p, mut t) = (0, 0);
|
||||
// The position of the last `*` in the pattern and the text position it was tried at.
|
||||
let mut backtrack: Option<(usize, usize)> = None;
|
||||
|
||||
while t < text.len() {
|
||||
if p < pattern.len() && pattern[p] == b'*' {
|
||||
// Try matching the `*` against nothing first, remembering where to resume.
|
||||
backtrack = Some((p, t));
|
||||
p += 1;
|
||||
} else if p < pattern.len() && pattern[p] == text[t] {
|
||||
p += 1;
|
||||
t += 1;
|
||||
} else if let Some((star, star_text)) = backtrack {
|
||||
// Let the last `*` consume one more character and try again.
|
||||
p = star + 1;
|
||||
t = star_text + 1;
|
||||
backtrack = Some((star, star_text + 1));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining pattern must be made of `*` only.
|
||||
pattern[p..].iter().all(|item| *item == b'*')
|
||||
}
|
||||
|
||||
/// Determines whether every key in `rule` is present in `values` with a value
|
||||
/// that matches the glob pattern from `rule`. An empty rule never matches,
|
||||
/// so that an empty rule cannot accidentally match everything.
|
||||
pub fn rule_matches(rule: &BTreeMap<String, String>, values: &BTreeMap<String, String>) -> bool {
|
||||
!rule.is_empty()
|
||||
&& rule.iter().all(|(key, pattern)| {
|
||||
values
|
||||
.get(key)
|
||||
.is_some_and(|value| glob_match(pattern, value))
|
||||
})
|
||||
}
|
||||
|
||||
/// A single combination of variant choices produced by [build_variants].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct VariantCombination {
|
||||
/// The names of the chosen choices, in axis order.
|
||||
pub names: Vec<String>,
|
||||
/// The values to insert into the context for this combination.
|
||||
/// Each axis name is set to the name of its chosen choice, and then
|
||||
/// the values of each chosen choice are inserted.
|
||||
pub values: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
/// A variant axis, which is a name and a list of choices.
|
||||
/// Each choice is a name and the values it inserts.
|
||||
pub type VariantAxis = (String, Vec<(String, BTreeMap<String, String>)>);
|
||||
|
||||
/// Builds every combination of the variant `axes`.
|
||||
/// Each axis is a name and a list of choices, where each choice is a name and a set of values.
|
||||
/// Combinations are produced in axis order, with the last axis varying fastest, and the
|
||||
/// choices of each axis in the order they are declared.
|
||||
/// No axes produce a single empty combination. An axis without any choices produces nothing.
|
||||
pub fn build_variants(axes: &[VariantAxis]) -> Vec<VariantCombination> {
|
||||
let mut result = alloc::vec![VariantCombination {
|
||||
names: Vec::new(),
|
||||
values: BTreeMap::new(),
|
||||
}];
|
||||
|
||||
for (axis, choices) in axes {
|
||||
let mut next = Vec::new();
|
||||
for combination in &result {
|
||||
for (name, values) in choices {
|
||||
let mut combination = combination.clone();
|
||||
combination.names.push(name.clone());
|
||||
combination.values.insert(axis.clone(), name.clone());
|
||||
combination
|
||||
.values
|
||||
.extend(values.iter().map(|(k, v)| (k.clone(), v.clone())));
|
||||
next.push(combination);
|
||||
}
|
||||
}
|
||||
result = next;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -484,4 +575,121 @@ mod tests {
|
||||
let candidates: Vec<_> = initramfs_candidates("-6.1.0", &[]).collect();
|
||||
assert!(candidates.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glob_without_star_is_exact() {
|
||||
assert!(glob_match("abc", "abc"));
|
||||
assert!(!glob_match("abc", "abcd"));
|
||||
assert!(!glob_match("abc", "ab"));
|
||||
assert!(glob_match("", ""));
|
||||
assert!(!glob_match("", "a"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glob_trailing_star_is_prefix() {
|
||||
assert!(glob_match("fedora-*", "fedora-6.1"));
|
||||
assert!(glob_match("fedora-*", "fedora-"));
|
||||
assert!(!glob_match("fedora-*", "linux-fedora-6.1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glob_star_anywhere() {
|
||||
assert!(glob_match(
|
||||
"linux-*-graphics-debug",
|
||||
"linux-abc-6.1-graphics-debug"
|
||||
));
|
||||
assert!(!glob_match(
|
||||
"linux-*-graphics-debug",
|
||||
"linux-abc-6.1-serial-debug"
|
||||
));
|
||||
assert!(glob_match("*rescue*", "0-rescue-abc"));
|
||||
assert!(glob_match("*", ""));
|
||||
assert!(glob_match("a**b", "ab"));
|
||||
assert!(glob_match("*a*a*", "banana"));
|
||||
assert!(!glob_match("*a*a*a*a*", "banana"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rule_requires_all_keys() {
|
||||
let values = map(&[("console", "graphics"), ("mode", "debug")]);
|
||||
assert!(rule_matches(
|
||||
&map(&[("console", "graphics"), ("mode", "deb*")]),
|
||||
&values
|
||||
));
|
||||
assert!(!rule_matches(
|
||||
&map(&[("console", "graphics"), ("mode", "normal")]),
|
||||
&values
|
||||
));
|
||||
assert!(!rule_matches(&map(&[("missing", "*")]), &values));
|
||||
assert!(!rule_matches(&BTreeMap::new(), &values));
|
||||
}
|
||||
|
||||
fn axis(name: &str, choices: &[(&str, &[(&str, &str)])]) -> VariantAxis {
|
||||
(
|
||||
name.to_string(),
|
||||
choices
|
||||
.iter()
|
||||
.map(|(choice, values)| (choice.to_string(), map(values)))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variants_without_axes_produce_one_empty_combination() {
|
||||
let result = build_variants(&[]);
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result[0].names.is_empty());
|
||||
assert!(result[0].values.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variants_with_empty_axis_produce_nothing() {
|
||||
let result = build_variants(&[axis("a", &[("x", &[])]), axis("b", &[])]);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variants_are_ordered_and_set_values() {
|
||||
let result = build_variants(&[
|
||||
axis(
|
||||
"console",
|
||||
&[
|
||||
("serial", &[("console-title", "Serial")]),
|
||||
("graphics", &[("console-title", "Graphics")]),
|
||||
],
|
||||
),
|
||||
axis(
|
||||
"mode",
|
||||
&[("normal", &[]), ("debug", &[("mode-title", " Debug")])],
|
||||
),
|
||||
]);
|
||||
let names: Vec<String> = result.iter().map(|c| c.names.join("-")).collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
[
|
||||
"serial-normal",
|
||||
"serial-debug",
|
||||
"graphics-normal",
|
||||
"graphics-debug"
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
result[3].values,
|
||||
map(&[
|
||||
("console", "graphics"),
|
||||
("console-title", "Graphics"),
|
||||
("mode", "debug"),
|
||||
("mode-title", " Debug")
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variant_choice_values_override_axis_name() {
|
||||
let result = build_variants(&[axis("console", &[("serial", &[("console", "custom")])])]);
|
||||
assert_eq!(
|
||||
result[0].values.get("console").map(String::as_str),
|
||||
Some("custom")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user