-
Notifications
You must be signed in to change notification settings - Fork 240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow specifying env vars for examples/HIL tests #3028
Open
bugadani
wants to merge
4
commits into
esp-rs:main
Choose a base branch
from
bugadani:hil-config
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+454
−235
Open
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,316 @@ | ||
use std::{ | ||
collections::HashMap, | ||
fs, | ||
path::{Path, PathBuf}, | ||
}; | ||
|
||
use anyhow::{bail, Context, Result}; | ||
use clap::ValueEnum; | ||
use esp_metadata::Chip; | ||
use strum::IntoEnumIterator as _; | ||
|
||
use crate::windows_safe_path; | ||
|
||
/// A single, configured example (or test). | ||
|
||
#[derive(Debug, Clone)] | ||
pub struct Metadata { | ||
example_path: PathBuf, | ||
chip: Chip, | ||
configuration_name: String, | ||
features: Vec<String>, | ||
tag: Option<String>, | ||
description: Option<String>, | ||
env_vars: HashMap<String, String>, | ||
} | ||
|
||
impl Metadata { | ||
/// Absolute path to the example. | ||
pub fn example_path(&self) -> &Path { | ||
&self.example_path | ||
} | ||
|
||
/// Name of the example. | ||
pub fn binary_name(&self) -> String { | ||
self.example_path() | ||
.file_name() | ||
.unwrap() | ||
.to_string_lossy() | ||
.replace(".rs", "") | ||
} | ||
|
||
/// Name of the example, including the name of the configuration. | ||
pub fn output_file_name(&self) -> String { | ||
if self.configuration_name.is_empty() { | ||
self.binary_name() | ||
} else { | ||
format!("{}_{}", self.binary_name(), self.configuration_name) | ||
} | ||
} | ||
|
||
/// The name of the configuration. | ||
pub fn configuration(&self) -> &str { | ||
&self.configuration_name | ||
} | ||
|
||
/// Name of the example, including the name of the configuration. | ||
pub fn name_with_configuration(&self) -> String { | ||
if self.configuration_name.is_empty() { | ||
self.binary_name() | ||
} else { | ||
format!("{} ({})", self.binary_name(), self.configuration_name) | ||
} | ||
} | ||
|
||
/// A list of all features required for building a given example. | ||
pub fn feature_set(&self) -> &[String] { | ||
&self.features | ||
} | ||
|
||
/// A list of all env vars to build a given example. | ||
pub fn env_vars(&self) -> &HashMap<String, String> { | ||
&self.env_vars | ||
} | ||
|
||
/// If the specified chip is in the list of chips, then it is supported. | ||
pub fn supports_chip(&self, chip: Chip) -> bool { | ||
self.chip == chip | ||
} | ||
|
||
/// Optional tag of the example. | ||
pub fn tag(&self) -> Option<String> { | ||
self.tag.clone() | ||
} | ||
|
||
/// Optional description of the example. | ||
pub fn description(&self) -> Option<String> { | ||
self.description.clone() | ||
} | ||
|
||
pub fn matches(&self, filter: &Option<String>) -> bool { | ||
let Some(filter) = filter.as_deref() else { | ||
return false; | ||
}; | ||
|
||
filter == self.binary_name() || filter == self.output_file_name() | ||
} | ||
} | ||
|
||
#[derive(Debug, Default, Clone)] | ||
pub struct Configuration { | ||
chips: Vec<Chip>, | ||
name: String, | ||
features: Vec<String>, | ||
esp_config: HashMap<String, String>, | ||
tag: Option<String>, | ||
} | ||
|
||
struct ConfigurationCollector<'a> { | ||
configurations: &'a mut HashMap<String, Configuration>, | ||
all_configurations: &'a mut Configuration, | ||
meta_line: &'a MetaLine, | ||
} | ||
|
||
impl ConfigurationCollector<'_> { | ||
fn apply(&mut self, callback: impl Fn(&mut Configuration)) { | ||
if self.meta_line.config_names.is_empty() { | ||
callback(self.all_configurations); | ||
} else { | ||
for config_name in &self.meta_line.config_names { | ||
let meta = self | ||
.configurations | ||
.entry(config_name.clone()) | ||
.or_insert_with(|| Configuration { | ||
name: config_name.clone(), | ||
..Configuration::default() | ||
}); | ||
callback(meta); | ||
} | ||
} | ||
} | ||
} | ||
|
||
struct MetaLine { | ||
key: String, | ||
config_names: Vec<String>, | ||
value: String, | ||
} | ||
|
||
/// Parse a metadata line from an example file. | ||
/// | ||
/// Metadata lines come in the form of: | ||
/// | ||
/// - `//% METADATA_KEY: value` or | ||
/// - `//% METADATA_KEY(config_name_1, config_name_2, ...): value`. | ||
fn parse_meta_line(line: &str) -> anyhow::Result<MetaLine> { | ||
let Some((key, value)) = line.trim_start_matches("//%").split_once(':') else { | ||
bail!("Metadata line is missing ':': {}", line); | ||
}; | ||
|
||
let (key, config_names) = if let Some((key, config_names)) = key.split_once('(') { | ||
let config_names = config_names | ||
.trim_end_matches(')') | ||
.split(',') | ||
.map(str::trim) | ||
.map(ToString::to_string) | ||
.collect(); | ||
(key.trim(), config_names) | ||
} else { | ||
(key, Vec::new()) | ||
}; | ||
|
||
let key = key.trim(); | ||
let value = value.trim(); | ||
|
||
Ok(MetaLine { | ||
key: key.to_string(), | ||
config_names, | ||
value: value.to_string(), | ||
}) | ||
} | ||
|
||
/// Load all examples at the given path, and parse their metadata. | ||
pub fn load(path: &Path) -> Result<Vec<Metadata>> { | ||
let mut examples = Vec::new(); | ||
|
||
for entry in fs::read_dir(path)? { | ||
let path = windows_safe_path(&entry?.path()); | ||
let text = fs::read_to_string(&path) | ||
.with_context(|| format!("Could not read {}", path.display()))?; | ||
|
||
let mut description = None; | ||
|
||
// collect `//!` as description | ||
for line in text.lines().filter(|line| line.starts_with("//!")) { | ||
let line = line.trim_start_matches("//!"); | ||
let mut descr: String = description.unwrap_or_default(); | ||
descr.push_str(line); | ||
descr.push('\n'); | ||
description = Some(descr); | ||
} | ||
|
||
// When the list of configuration names is missing, the metadata is applied to | ||
// all configurations. Each configuration encountered will create a | ||
// separate Metadata entry. Different metadata lines referring to the | ||
// same configuration will be merged. | ||
// | ||
// If there are no named configurations, an unnamed default is created. | ||
let mut all_configuration = Configuration { | ||
chips: Chip::iter().collect::<Vec<_>>(), | ||
..Configuration::default() | ||
}; | ||
|
||
let mut configurations = HashMap::<String, Configuration>::new(); | ||
|
||
// Unless specified, an example is assumed to be valid for all chips. | ||
for (line_no, line) in text | ||
.lines() | ||
.enumerate() | ||
.filter(|(_, line)| line.starts_with("//%")) | ||
{ | ||
let meta_line = parse_meta_line(line) | ||
.with_context(|| format!("Failed to parse line {}", line_no + 1))?; | ||
|
||
let mut relevant_metadata = ConfigurationCollector { | ||
configurations: &mut configurations, | ||
all_configurations: &mut all_configuration, | ||
meta_line: &meta_line, | ||
}; | ||
|
||
match meta_line.key.as_str() { | ||
// A list of chips that can run the example using the current configuration. | ||
"CHIPS" => { | ||
let chips = meta_line | ||
.value | ||
.split_ascii_whitespace() | ||
.map(|s| Chip::from_str(s, false).unwrap()) | ||
.collect::<Vec<_>>(); | ||
relevant_metadata.apply(|meta| meta.chips = chips.clone()); | ||
} | ||
// Cargo features to enable for the current configuration. | ||
"FEATURES" => { | ||
let mut values = meta_line | ||
.value | ||
.split_ascii_whitespace() | ||
.map(ToString::to_string) | ||
.collect::<Vec<_>>(); | ||
|
||
// Sort the features so they are in a deterministic order: | ||
values.sort(); | ||
|
||
relevant_metadata.apply(|meta| meta.features.extend_from_slice(&values)); | ||
} | ||
// esp-config env vars, one per line | ||
"ENV" => { | ||
let (env_var, value) = meta_line | ||
.value | ||
.split_once('=') | ||
.with_context(|| "CONFIG metadata must be in the form 'CONFIG=VALUE'")?; | ||
|
||
let env_var = env_var.trim(); | ||
let value = value.trim(); | ||
|
||
relevant_metadata.apply(|meta| { | ||
meta.esp_config | ||
.insert(env_var.to_string(), value.to_string()); | ||
}); | ||
} | ||
// Tags by which the user can filter examples. | ||
"TAG" => { | ||
relevant_metadata.apply(|meta| meta.tag = Some(meta_line.value.to_string())); | ||
} | ||
key => log::warn!("Unrecognized metadata key '{key}', ignoring"), | ||
} | ||
} | ||
|
||
// Merge "all" into configurations | ||
for meta in configurations.values_mut() { | ||
// Chips is a filter, inherit if empty | ||
if meta.chips.is_empty() { | ||
meta.chips = all_configuration.chips.clone(); | ||
} | ||
|
||
// Tag is an ID, inherit if empty | ||
if meta.tag.is_none() { | ||
meta.tag = all_configuration.tag.clone(); | ||
} | ||
|
||
// Other values are merged | ||
meta.features.extend_from_slice(&all_configuration.features); | ||
meta.esp_config.extend(all_configuration.esp_config.clone()); | ||
} | ||
|
||
// If no configurations are specified, fall back to the unnamed one. Otherwise | ||
// ignore it, it has been merged into the others. | ||
if configurations.is_empty() { | ||
configurations.insert(String::new(), all_configuration); | ||
} | ||
|
||
// Generate metadata | ||
|
||
for configuration in configurations.values_mut() { | ||
// Sort the features so they are in a deterministic order: | ||
configuration.features.sort(); | ||
|
||
for chip in &configuration.chips { | ||
examples.push(Metadata { | ||
// File properties | ||
example_path: path.clone(), | ||
description: description.clone(), | ||
|
||
// Configuration | ||
chip: *chip, | ||
configuration_name: configuration.name.clone(), | ||
features: configuration.features.clone(), | ||
tag: configuration.tag.clone(), | ||
env_vars: configuration.esp_config.clone(), | ||
}) | ||
} | ||
} | ||
} | ||
|
||
// Sort by feature set, to prevent rebuilding packages if not necessary. | ||
examples.sort_by_key(|e| e.feature_set().join(",")); | ||
|
||
Ok(examples) | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we get some docs somewhere on how to use this? 🥺 I think I understand now, but it wasn't immediately clear. I think a new contributor would also struggle to grok this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or maybe less how to use it, but more what this does to the tests we're about to run
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically it's in the code, but in bits and pieces, so I'll try to collect it into the readme.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
then probably it's worth to add documentation about the meta-keys
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can do that but I don't understand TAG I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TAG is just a name and only used to sort examples for
run-example
w/o specifying an example (i.e. run all examples one after each other) - but we can leave that out and I document that one (since I added it 😄 )There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added docs to xtask/README.md, let me know if they make sense.