Skip to content
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
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
8 changes: 7 additions & 1 deletion hil-test/tests/embassy_interrupt_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
//% CHIPS: esp32 esp32c2 esp32c3 esp32c6 esp32h2 esp32s2 esp32s3
//% FEATURES: unstable embassy
//% ENV(single_integrated): ESP_HAL_EMBASSY_CONFIG_TIMER_QUEUE = single-integrated
Copy link
Member

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.

Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

@bjoernQ bjoernQ Jan 24, 2025

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 😄 )

Copy link
Contributor Author

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.

//% ENV(multiple_integrated): ESP_HAL_EMBASSY_CONFIG_TIMER_QUEUE = multiple-integrated
//% ENV(generic_queue): ESP_HAL_EMBASSY_CONFIG_TIMER_QUEUE = generic
//% ENV(generic_queue): ESP_HAL_EMBASSY_CONFIG_GENERIC_QUEUE_SIZE = 16
//% ENV(default_with_waiti): ESP_HAL_EMBASSY_CONFIG_LOW_POWER_WAIT = true
//% ENV(default_no_waiti): ESP_HAL_EMBASSY_CONFIG_LOW_POWER_WAIT = false

#![no_std]
#![no_main]
Expand Down Expand Up @@ -50,7 +56,7 @@ struct Context {
}

#[cfg(test)]
#[embedded_test::tests(default_timeout = 3, executor = hil_test::Executor::new())]
#[embedded_test::tests(default_timeout = 3, executor = esp_hal_embassy::Executor::new())]
mod test {
use super::*;

Expand Down
4 changes: 4 additions & 0 deletions hil-test/tests/embassy_interrupt_spi_dma.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

//% CHIPS: esp32 esp32s2 esp32s3 esp32c3 esp32c6 esp32h2
//% FEATURES: unstable embassy
//% ENV(single_integrated): ESP_HAL_EMBASSY_CONFIG_TIMER_QUEUE = single-integrated
//% ENV(multiple_integrated): ESP_HAL_EMBASSY_CONFIG_TIMER_QUEUE = multiple-integrated
//% ENV(generic_queue): ESP_HAL_EMBASSY_CONFIG_TIMER_QUEUE = generic
//% ENV(generic_queue): ESP_HAL_EMBASSY_CONFIG_GENERIC_QUEUE_SIZE = 16

#![no_std]
#![no_main]
Expand Down
2 changes: 1 addition & 1 deletion hil-test/tests/gpio.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! GPIO Test
//% CHIPS: esp32 esp32c2 esp32c3 esp32c6 esp32h2 esp32s2 esp32s3
//% FEATURES: unstable embassy
//% FEATURES(unstable): unstable embassy
//% FEATURES(stable):

#![no_std]
Expand Down
5 changes: 0 additions & 5 deletions xtask/src/cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@ pub fn run(args: &[String], cwd: &Path) -> Result<()> {
Ok(())
}

/// Execute cargo with the given arguments and from the specified directory.
pub fn run_and_capture(args: &[String], cwd: &Path) -> Result<String> {
run_with_env::<[(&str, &str); 0], _, _>(args, cwd, [], true)
}

/// Execute cargo with the given arguments and from the specified directory.
pub fn run_with_env<I, K, V>(args: &[String], cwd: &Path, envs: I, capture: bool) -> Result<String>
where
Expand Down
316 changes: 316 additions & 0 deletions xtask/src/firmware.rs
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)
}
Loading
Loading