1use core::fmt::Display;
2use std::{collections::HashMap, env, fmt, fs, io::Write, path::PathBuf};
3
4use serde::{Deserialize, Serialize};
5use somni_expr::TypeSet128;
6
7use crate::generate::{validator::Validator, value::Value};
8
9mod markdown;
10pub(crate) mod validator;
11pub(crate) mod value;
12
13#[derive(Clone, PartialEq, Eq)]
15pub enum Error {
16 Parse(String),
18 Validation(String),
20}
21
22impl Error {
23 pub fn parse<S>(message: S) -> Self
25 where
26 S: Into<String>,
27 {
28 Self::Parse(message.into())
29 }
30
31 pub fn validation<S>(message: S) -> Self
33 where
34 S: Into<String>,
35 {
36 Self::Validation(message.into())
37 }
38}
39
40impl fmt::Debug for Error {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 write!(f, "{self}")
43 }
44}
45
46impl fmt::Display for Error {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 Error::Parse(message) => write!(f, "{message}"),
50 Error::Validation(message) => write!(f, "{message}"),
51 }
52 }
53}
54
55impl std::error::Error for Error {
56 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
57 None
58 }
59
60 fn description(&self) -> &str {
61 "description() is deprecated; use Display"
62 }
63
64 fn cause(&self) -> Option<&dyn core::error::Error> {
65 self.source()
66 }
67}
68
69#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
71#[serde(deny_unknown_fields)]
72pub struct Config {
73 #[serde(rename = "crate")]
75 pub krate: String,
76 pub options: Vec<CfgOption>,
78 pub checks: Option<Vec<String>>,
80}
81
82fn true_default() -> String {
83 "true".to_string()
84}
85
86fn unstable_default() -> Stability {
87 Stability::Unstable
88}
89
90#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
92#[serde(deny_unknown_fields)]
93pub struct CfgDefaultValue {
94 #[serde(rename = "if")]
97 #[serde(default = "true_default")]
98 pub if_: String,
99 pub value: Value,
101}
102
103#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
105#[serde(deny_unknown_fields)]
106pub struct CfgOption {
107 pub name: String,
109 pub description: String,
112 #[serde(default = "true_default")]
114 pub active: String,
115 pub default: Vec<CfgDefaultValue>,
118 pub constraints: Option<Vec<CfgConstraint>>,
121 pub display_hint: Option<DisplayHint>,
124 #[serde(default = "unstable_default")]
126 pub stability: Stability,
127}
128
129#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
131#[serde(deny_unknown_fields)]
132pub struct CfgConstraint {
133 #[serde(rename = "if")]
135 #[serde(default = "true_default")]
136 if_: String,
137 #[serde(rename = "type")]
139 type_: Validator,
140}
141
142pub fn generate_config_from_yaml_definition(
147 yaml: &str,
148 enable_unstable: bool,
149 emit_md_tables: bool,
150 chip: Option<esp_metadata_generated::Chip>,
151) -> Result<HashMap<String, Value>, Error> {
152 let features: Vec<String> = env::vars()
153 .filter(|(k, _)| k.starts_with("CARGO_FEATURE_"))
154 .map(|(k, _)| k)
155 .map(|v| {
156 v.strip_prefix("CARGO_FEATURE_")
157 .unwrap_or_default()
158 .to_string()
159 })
160 .collect();
161
162 let (config, options) = evaluate_yaml_config(yaml, chip, features, false)?;
163
164 let cfg = generate_config(&config.krate, &options, enable_unstable, emit_md_tables);
165
166 do_checks(config.checks.as_ref(), &cfg)?;
167
168 Ok(cfg)
169}
170
171pub fn do_checks(checks: Option<&Vec<String>>, cfg: &HashMap<String, Value>) -> Result<(), Error> {
173 if let Some(checks) = checks {
174 let mut eval_ctx = somni_expr::Context::<TypeSet128>::new_with_types();
175 for (k, v) in cfg.iter() {
176 match v {
177 Value::Bool(v) => eval_ctx.add_variable(k, *v),
178 Value::Integer(v) => eval_ctx.add_variable(k, *v),
179 Value::String(v) => eval_ctx.add_variable::<&str>(k, v),
180 }
181 }
182 for check in checks {
183 if !eval_ctx
184 .evaluate::<bool>(check)
185 .map_err(|err| Error::Parse(format!("Validation error: {err:?}")))?
186 {
187 return Err(Error::Validation(format!("Validation error: '{check}'")));
188 }
189 }
190 };
191 Ok(())
192}
193
194pub fn evaluate_yaml_config(
196 yaml: &str,
197 chip: Option<esp_metadata_generated::Chip>,
198 features: Vec<String>,
199 ignore_feature_gates: bool,
200) -> Result<(Config, Vec<ConfigOption>), Error> {
201 let config: Config = serde_yaml::from_str(yaml).map_err(|err| Error::Parse(err.to_string()))?;
202 let mut options = Vec::new();
203 let mut eval_ctx = somni_expr::Context::new();
204
205 for c in esp_metadata_generated::Chip::iter() {
206 if chip != Some(c) {
207 eval_ctx.add_variable(c.name(), false);
208 for symbol in c.all_symbols() {
209 if let Some((key, _value)) = symbol.split_once('=') {
210 eval_ctx.add_variable(key.trim(), "");
211 } else {
212 eval_ctx.add_variable(symbol, false);
213 }
214 }
215 }
216 }
217
218 if let Some(c) = chip {
219 eval_ctx.add_variable(c.name(), true);
220 for symbol in c.all_symbols() {
221 if let Some((key, value)) = symbol.split_once('=') {
222 let value = value.trim().trim_matches('"');
223 eval_ctx.add_variable(key.trim(), value);
224 } else {
225 eval_ctx.add_variable(symbol, true);
226 }
227 }
228 }
229
230 if chip.is_some() {
231 eval_ctx.add_variable("ignore_feature_gates", ignore_feature_gates);
232 eval_ctx.add_function("cargo_feature", |feature: &str| {
233 features.contains(&feature.to_uppercase().replace("-", "_"))
234 });
235 }
236 for option in &config.options {
237 let active = eval_ctx
238 .evaluate::<bool>(&option.active)
239 .map_err(|err| Error::Parse(format!("{err:?}")))?;
240
241 let constraint = {
242 let mut active_constraint = None;
243 if let Some(constraints) = &option.constraints {
244 for constraint in constraints {
245 if eval_ctx
246 .evaluate::<bool>(&constraint.if_)
247 .map_err(|err| Error::Parse(format!("{err:?}")))?
248 {
249 active_constraint = Some(constraint.type_.clone());
250 break;
251 }
252 }
253 };
254
255 if option.constraints.is_some() && active_constraint.is_none() {
256 panic!(
257 "No constraint active for crate {}, option {}",
258 config.krate, option.name
259 );
260 }
261
262 active_constraint
263 };
264
265 let default_value = {
266 let mut default_value = None;
267 for value in &option.default {
268 if eval_ctx
269 .evaluate::<bool>(&value.if_)
270 .map_err(|err| Error::Parse(format!("{err:?}")))?
271 {
272 default_value = Some(value.value.clone());
273 break;
274 }
275 }
276
277 if default_value.is_none() {
278 panic!(
279 "No default value active for crate {}, option {}",
280 config.krate, option.name
281 );
282 }
283
284 default_value
285 };
286
287 let option = ConfigOption {
288 name: option.name.clone(),
289 description: option.description.clone(),
290 default_value: default_value.ok_or_else(|| {
291 Error::Parse(format!("No default value found for {}", option.name))
292 })?,
293 constraint,
294 stability: option.stability.clone(),
295 active,
296 display_hint: option.display_hint.unwrap_or(DisplayHint::None),
297 };
298 options.push(option);
299 }
300 Ok((config, options))
301}
302
303pub fn generate_config(
326 crate_name: &str,
327 config: &[ConfigOption],
328 enable_unstable: bool,
329 emit_md_tables: bool,
330) -> HashMap<String, Value> {
331 let configs = generate_config_internal(std::io::stdout(), crate_name, config, enable_unstable);
332
333 if emit_md_tables {
334 let file_name = snake_case(crate_name);
335
336 let mut doc_table = markdown::DOC_TABLE_HEADER.replace(
337 "{prefix}",
338 format!("{}_CONFIG_*", screaming_snake_case(crate_name)).as_str(),
339 );
340 let mut selected_config = String::from(markdown::SELECTED_TABLE_HEADER);
341
342 for (name, option, value) in configs.iter() {
343 if !option.active {
344 continue;
345 }
346 markdown::write_doc_table_line(&mut doc_table, name, option);
347 markdown::write_summary_table_line(&mut selected_config, name, value);
348 }
349
350 write_out_file(format!("{file_name}_config_table.md"), doc_table);
351 write_out_file(format!("{file_name}_selected_config.md"), selected_config);
352 }
353
354 configs.into_iter().map(|(k, _, v)| (k, v)).collect()
356}
357
358pub fn generate_config_internal<'a>(
359 mut stdout: impl Write,
360 crate_name: &str,
361 config: &'a [ConfigOption],
362 enable_unstable: bool,
363) -> Vec<(String, &'a ConfigOption, Value)> {
364 writeln!(stdout, "cargo:rerun-if-changed=build.rs").ok();
367
368 let prefix = format!("{}_CONFIG_", screaming_snake_case(crate_name));
370
371 let mut configs = create_config(&prefix, config);
372 capture_from_env(crate_name, &prefix, &mut configs, enable_unstable);
373
374 for (_, option, value) in configs.iter() {
375 if let Some(ref validator) = option.constraint {
376 validator.validate(value).unwrap_or_else(|err| {
377 panic!(
378 "Validation error for crate {}, option {}: {err}",
379 crate_name, option.name
380 )
381 });
382 }
383 }
384
385 emit_configuration(&mut stdout, &configs);
386
387 configs
388}
389
390#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
392pub enum Stability {
393 Unstable,
396 Stable(String),
399}
400
401impl Display for Stability {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 match self {
404 Stability::Unstable => write!(f, "⚠️ Unstable"),
405 Stability::Stable(version) => write!(f, "Stable since {version}"),
406 }
407 }
408}
409
410#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
412pub enum DisplayHint {
413 None,
415
416 Binary,
418
419 Hex,
421
422 Octal,
424}
425
426impl DisplayHint {
427 pub fn format_value(self, value: &Value) -> String {
429 match value {
430 Value::Bool(b) => b.to_string(),
431 Value::Integer(i) => match self {
432 DisplayHint::None => format!("{i}"),
433 DisplayHint::Binary => format!("0b{i:0b}"),
434 DisplayHint::Hex => format!("0x{i:X}"),
435 DisplayHint::Octal => format!("0o{i:o}"),
436 },
437 Value::String(s) => s.clone(),
438 }
439 }
440}
441
442#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
444pub struct ConfigOption {
445 pub name: String,
450
451 pub description: String,
456
457 pub default_value: Value,
459
460 pub constraint: Option<Validator>,
462
463 pub stability: Stability,
465
466 pub active: bool,
471
472 pub display_hint: DisplayHint,
474}
475
476impl ConfigOption {
477 pub fn full_env_var(&self, crate_name: &str) -> String {
479 self.env_var(&format!("{}_CONFIG_", screaming_snake_case(crate_name)))
480 }
481
482 fn env_var(&self, prefix: &str) -> String {
483 format!("{}{}", prefix, screaming_snake_case(&self.name))
484 }
485
486 fn cfg_name(&self) -> String {
487 snake_case(&self.name)
488 }
489
490 fn is_stable(&self) -> bool {
491 matches!(self.stability, Stability::Stable(_))
492 }
493}
494
495fn create_config<'a>(
496 prefix: &str,
497 config: &'a [ConfigOption],
498) -> Vec<(String, &'a ConfigOption, Value)> {
499 let mut configs = Vec::with_capacity(config.len());
500
501 for option in config {
502 configs.push((option.env_var(prefix), option, option.default_value.clone()));
503 }
504
505 configs
506}
507
508fn capture_from_env(
509 crate_name: &str,
510 prefix: &str,
511 configs: &mut Vec<(String, &ConfigOption, Value)>,
512 enable_unstable: bool,
513) {
514 let mut unknown = Vec::new();
515 let mut failed = Vec::new();
516 let mut unstable = Vec::new();
517
518 for (var, value) in env::vars() {
520 if var.starts_with(prefix) {
521 let Some((_, option, cfg)) = configs.iter_mut().find(|(k, _, _)| k == &var) else {
522 unknown.push(var);
523 continue;
524 };
525
526 if !option.active {
527 unknown.push(var);
528 continue;
529 }
530
531 if !enable_unstable && !option.is_stable() {
532 unstable.push(var);
533 continue;
534 }
535
536 if let Err(e) = cfg.parse_in_place(&value) {
537 failed.push(format!("{var}: {e}"));
538 }
539 }
540 }
541
542 if !failed.is_empty() {
543 panic!("Invalid configuration options detected: {failed:?}");
544 }
545
546 if !unstable.is_empty() {
547 panic!(
548 "The following configuration options are unstable: {unstable:?}. You can enable it by \
549 activating the 'unstable' feature in {crate_name}."
550 );
551 }
552
553 if !unknown.is_empty() {
554 panic!("Unknown configuration options detected: {unknown:?}");
555 }
556}
557
558fn emit_configuration(mut stdout: impl Write, configs: &[(String, &ConfigOption, Value)]) {
559 for (env_var_name, option, value) in configs.iter() {
560 let cfg_name = option.cfg_name();
561
562 writeln!(stdout, "cargo:rustc-env={env_var_name}={value}").ok();
566 writeln!(stdout, "cargo:rerun-if-env-changed={env_var_name}").ok();
567
568 writeln!(stdout, "cargo:rustc-check-cfg=cfg({cfg_name})").ok();
570
571 if let Value::Bool(true) = value {
573 writeln!(stdout, "cargo:rustc-cfg={cfg_name}").ok();
574 }
575
576 if let Some(validator) = option.constraint.as_ref() {
578 validator.emit_cargo_extras(&mut stdout, &cfg_name, value);
579 }
580 }
581}
582
583fn write_out_file(file_name: String, json: String) {
584 let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
585 let out_file = out_dir.join(file_name);
586 fs::write(out_file, json).unwrap();
587}
588
589fn snake_case(name: &str) -> String {
590 let mut name = name.replace("-", "_");
591 name.make_ascii_lowercase();
592
593 name
594}
595
596fn screaming_snake_case(name: &str) -> String {
597 let mut name = name.replace("-", "_");
598 name.make_ascii_uppercase();
599
600 name
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606 use crate::generate::{validator::Validator, value::Value};
607
608 #[test]
609 fn value_number_formats() {
610 const INPUTS: &[&str] = &["0xAA", "0o252", "0b0000000010101010", "170"];
611 let mut v = Value::Integer(0);
612
613 for input in INPUTS {
614 v.parse_in_place(input).unwrap();
615 assert_eq!(v.to_string(), "170");
617 }
618 }
619
620 #[test]
621 fn value_bool_inputs() {
622 let mut v = Value::Bool(false);
623
624 v.parse_in_place("true").unwrap();
625 assert_eq!(v.to_string(), "true");
626
627 v.parse_in_place("false").unwrap();
628 assert_eq!(v.to_string(), "false");
629
630 v.parse_in_place("else")
631 .expect_err("Only true or false are valid");
632 }
633
634 #[test]
635 fn env_override() {
636 temp_env::with_vars(
637 [
638 ("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
639 ("ESP_TEST_CONFIG_NUMBER_SIGNED", Some("-999")),
640 ("ESP_TEST_CONFIG_STRING", Some("Hello world!")),
641 ("ESP_TEST_CONFIG_BOOL", Some("true")),
642 ],
643 || {
644 let configs = generate_config(
645 "esp-test",
646 &[
647 ConfigOption {
648 name: String::from("number"),
649 description: String::from("NA"),
650 default_value: Value::Integer(999),
651 constraint: None,
652 stability: Stability::Stable(String::from("testing")),
653 active: true,
654 display_hint: DisplayHint::None,
655 },
656 ConfigOption {
657 name: String::from("number_signed"),
658 description: String::from("NA"),
659 default_value: Value::Integer(-777),
660 constraint: None,
661 stability: Stability::Stable(String::from("testing")),
662 active: true,
663 display_hint: DisplayHint::None,
664 },
665 ConfigOption {
666 name: String::from("string"),
667 description: String::from("NA"),
668 default_value: Value::String("Demo".to_string()),
669 constraint: None,
670 stability: Stability::Stable(String::from("testing")),
671 active: true,
672 display_hint: DisplayHint::None,
673 },
674 ConfigOption {
675 name: String::from("bool"),
676 description: String::from("NA"),
677 default_value: Value::Bool(false),
678 constraint: None,
679 stability: Stability::Stable(String::from("testing")),
680 active: true,
681 display_hint: DisplayHint::None,
682 },
683 ConfigOption {
684 name: String::from("number_default"),
685 description: String::from("NA"),
686 default_value: Value::Integer(999),
687 constraint: None,
688 stability: Stability::Stable(String::from("testing")),
689 active: true,
690 display_hint: DisplayHint::None,
691 },
692 ConfigOption {
693 name: String::from("string_default"),
694 description: String::from("NA"),
695 default_value: Value::String("Demo".to_string()),
696 constraint: None,
697 stability: Stability::Stable(String::from("testing")),
698 active: true,
699 display_hint: DisplayHint::None,
700 },
701 ConfigOption {
702 name: String::from("bool_default"),
703 description: String::from("NA"),
704 default_value: Value::Bool(false),
705 constraint: None,
706 stability: Stability::Stable(String::from("testing")),
707 active: true,
708 display_hint: DisplayHint::None,
709 },
710 ],
711 false,
712 false,
713 );
714
715 assert_eq!(configs["ESP_TEST_CONFIG_NUMBER"], Value::Integer(0xaa));
717 assert_eq!(
718 configs["ESP_TEST_CONFIG_NUMBER_SIGNED"],
719 Value::Integer(-999)
720 );
721 assert_eq!(
722 configs["ESP_TEST_CONFIG_STRING"],
723 Value::String("Hello world!".to_string())
724 );
725 assert_eq!(configs["ESP_TEST_CONFIG_BOOL"], Value::Bool(true));
726
727 assert_eq!(
729 configs["ESP_TEST_CONFIG_NUMBER_DEFAULT"],
730 Value::Integer(999)
731 );
732 assert_eq!(
733 configs["ESP_TEST_CONFIG_STRING_DEFAULT"],
734 Value::String("Demo".to_string())
735 );
736 assert_eq!(configs["ESP_TEST_CONFIG_BOOL_DEFAULT"], Value::Bool(false));
737 },
738 )
739 }
740
741 #[test]
742 fn builtin_validation_passes() {
743 temp_env::with_vars(
744 [
745 ("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("7")),
746 ("ESP_TEST_CONFIG_NEGATIVE_NUMBER", Some("-1")),
747 ("ESP_TEST_CONFIG_NON_NEGATIVE_NUMBER", Some("0")),
748 ("ESP_TEST_CONFIG_RANGE", Some("9")),
749 ],
750 || {
751 generate_config(
752 "esp-test",
753 &[
754 ConfigOption {
755 name: String::from("positive_number"),
756 description: String::from("NA"),
757 default_value: Value::Integer(-1),
758 constraint: Some(Validator::PositiveInteger),
759 stability: Stability::Stable(String::from("testing")),
760 active: true,
761 display_hint: DisplayHint::None,
762 },
763 ConfigOption {
764 name: String::from("negative_number"),
765 description: String::from("NA"),
766 default_value: Value::Integer(1),
767 constraint: Some(Validator::NegativeInteger),
768 stability: Stability::Stable(String::from("testing")),
769 active: true,
770 display_hint: DisplayHint::None,
771 },
772 ConfigOption {
773 name: String::from("non_negative_number"),
774 description: String::from("NA"),
775 default_value: Value::Integer(-1),
776 constraint: Some(Validator::NonNegativeInteger),
777 stability: Stability::Stable(String::from("testing")),
778 active: true,
779 display_hint: DisplayHint::None,
780 },
781 ConfigOption {
782 name: String::from("range"),
783 description: String::from("NA"),
784 default_value: Value::Integer(0),
785 constraint: Some(Validator::IntegerInRange(5..10)),
786 stability: Stability::Stable(String::from("testing")),
787 active: true,
788 display_hint: DisplayHint::None,
789 },
790 ],
791 false,
792 false,
793 )
794 },
795 );
796 }
797
798 #[test]
799 #[should_panic]
800 fn builtin_validation_bails() {
801 temp_env::with_vars([("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("-99"))], || {
802 generate_config(
803 "esp-test",
804 &[ConfigOption {
805 name: String::from("positive_number"),
806 description: String::from("NA"),
807 default_value: Value::Integer(-1),
808 constraint: Some(Validator::PositiveInteger),
809 stability: Stability::Stable(String::from("testing")),
810 active: true,
811 display_hint: DisplayHint::None,
812 }],
813 false,
814 false,
815 )
816 });
817 }
818
819 #[test]
820 #[should_panic]
821 fn env_unknown_bails() {
822 temp_env::with_vars(
823 [
824 ("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
825 ("ESP_TEST_CONFIG_RANDOM_VARIABLE", Some("")),
826 ],
827 || {
828 generate_config(
829 "esp-test",
830 &[ConfigOption {
831 name: String::from("number"),
832 description: String::from("NA"),
833 default_value: Value::Integer(999),
834 constraint: None,
835 stability: Stability::Stable(String::from("testing")),
836 active: true,
837 display_hint: DisplayHint::None,
838 }],
839 false,
840 false,
841 );
842 },
843 );
844 }
845
846 #[test]
847 #[should_panic]
848 fn env_invalid_values_bails() {
849 temp_env::with_vars([("ESP_TEST_CONFIG_NUMBER", Some("Hello world"))], || {
850 generate_config(
851 "esp-test",
852 &[ConfigOption {
853 name: String::from("number"),
854 description: String::from("NA"),
855 default_value: Value::Integer(999),
856 constraint: None,
857 stability: Stability::Stable(String::from("testing")),
858 active: true,
859 display_hint: DisplayHint::None,
860 }],
861 false,
862 false,
863 );
864 });
865 }
866
867 #[test]
868 fn env_unknown_prefix_is_ignored() {
869 temp_env::with_vars(
870 [("ESP_TEST_OTHER_CONFIG_NUMBER", Some("Hello world"))],
871 || {
872 generate_config(
873 "esp-test",
874 &[ConfigOption {
875 name: String::from("number"),
876 description: String::from("NA"),
877 default_value: Value::Integer(999),
878 constraint: None,
879 stability: Stability::Stable(String::from("testing")),
880 active: true,
881 display_hint: DisplayHint::None,
882 }],
883 false,
884 false,
885 );
886 },
887 );
888 }
889
890 #[test]
891 fn enumeration_validator() {
892 let mut stdout = Vec::new();
893 temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
894 generate_config_internal(
895 &mut stdout,
896 "esp-test",
897 &[ConfigOption {
898 name: String::from("some-key"),
899 description: String::from("NA"),
900 default_value: Value::String("variant-0".to_string()),
901 constraint: Some(Validator::Enumeration(vec![
902 "variant-0".to_string(),
903 "variant-1".to_string(),
904 ])),
905 stability: Stability::Stable(String::from("testing")),
906 active: true,
907 display_hint: DisplayHint::None,
908 }],
909 false,
910 );
911 });
912
913 let cargo_lines: Vec<&str> = std::str::from_utf8(&stdout).unwrap().lines().collect();
914 assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key)"));
915 assert!(cargo_lines.contains(&"cargo:rustc-env=ESP_TEST_CONFIG_SOME_KEY=variant-0"));
916 assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_0)"));
917 assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_1)"));
918 assert!(cargo_lines.contains(&"cargo:rustc-cfg=some_key_variant_0"));
919 }
920
921 #[test]
922 #[should_panic]
923 fn unstable_option_panics_unless_enabled() {
924 let mut stdout = Vec::new();
925 temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
926 generate_config_internal(
927 &mut stdout,
928 "esp-test",
929 &[ConfigOption {
930 name: String::from("some-key"),
931 description: String::from("NA"),
932 default_value: Value::String("variant-0".to_string()),
933 constraint: Some(Validator::Enumeration(vec![
934 "variant-0".to_string(),
935 "variant-1".to_string(),
936 ])),
937 stability: Stability::Unstable,
938 active: true,
939 display_hint: DisplayHint::None,
940 }],
941 false,
942 );
943 });
944 }
945
946 #[test]
947 #[should_panic]
948 fn inactive_option_panics() {
949 let mut stdout = Vec::new();
950 temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
951 generate_config_internal(
952 &mut stdout,
953 "esp-test",
954 &[ConfigOption {
955 name: String::from("some-key"),
956 description: String::from("NA"),
957 default_value: Value::String("variant-0".to_string()),
958 constraint: Some(Validator::Enumeration(vec![
959 "variant-0".to_string(),
960 "variant-1".to_string(),
961 ])),
962 stability: Stability::Stable(String::from("testing")),
963 active: false,
964 display_hint: DisplayHint::None,
965 }],
966 false,
967 );
968 });
969 }
970
971 #[test]
972 fn deserialization() {
973 let yml = r#"
974crate: esp-bootloader-esp-idf
975
976options:
977- name: mmu_page_size
978 description: ESP32-C2, ESP32-C6 and ESP32-H2 support configurable page sizes. This is currently only used to populate the app descriptor.
979 default:
980 - value: '"64k"'
981 stability: !Stable xxxx
982 constraints:
983 - if: true
984 type:
985 validator: enumeration
986 value:
987 - 8k
988 - 16k
989 - 32k
990 - 64k
991
992- name: esp_idf_version
993 description: ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.
994 default:
995 - if: 'esp32c6'
996 value: '"esp32c6"'
997 - if: 'esp32'
998 value: '"other"'
999 active: true
1000
1001- name: partition-table-offset
1002 description: "The address of partition table (by default 0x8000). Allows you to \
1003 move the partition table, it gives more space for the bootloader. Note that the \
1004 bootloader and app will both need to be compiled with the same \
1005 PARTITION_TABLE_OFFSET value."
1006 default:
1007 - if: true
1008 value: 32768
1009 stability: Unstable
1010 active: 'esp32c6'
1011"#;
1012
1013 let (cfg, options) = evaluate_yaml_config(
1014 yml,
1015 Some(esp_metadata_generated::Chip::Esp32c6),
1016 vec![],
1017 false,
1018 )
1019 .unwrap();
1020
1021 assert_eq!("esp-bootloader-esp-idf", cfg.krate);
1022
1023 assert_eq!(
1024 vec![
1025 ConfigOption {
1026 name: "mmu_page_size".to_string(),
1027 description: "ESP32-C2, ESP32-C6 and ESP32-H2 support configurable page sizes. This is currently only used to populate the app descriptor.".to_string(),
1028 default_value: Value::String("64k".to_string()),
1029 constraint: Some(
1030 Validator::Enumeration(
1031 vec![
1032 "8k".to_string(),
1033 "16k".to_string(),
1034 "32k".to_string(),
1035 "64k".to_string(),
1036 ],
1037 ),
1038 ),
1039 stability: Stability::Stable("xxxx".to_string()),
1040 active: true,
1041 display_hint: DisplayHint::None,
1042 },
1043 ConfigOption {
1044 name: "esp_idf_version".to_string(),
1045 description: "ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.".to_string(),
1046 default_value: Value::String("esp32c6".to_string()),
1047 constraint: None,
1048 stability: Stability::Unstable,
1049 active: true,
1050 display_hint: DisplayHint::None,
1051 },
1052 ConfigOption {
1053 name: "partition-table-offset".to_string(),
1054 description: "The address of partition table (by default 0x8000). Allows you to move the partition table, it gives more space for the bootloader. Note that the bootloader and app will both need to be compiled with the same PARTITION_TABLE_OFFSET value.".to_string(),
1055 default_value: Value::Integer(32768),
1056 constraint: None,
1057 stability: Stability::Unstable,
1058 active: true,
1059 display_hint: DisplayHint::None,
1060 },
1061 ],
1062 options
1063 );
1064 }
1065
1066 #[test]
1067 fn deserialization_fallback_default() {
1068 let yml = r#"
1069crate: esp-bootloader-esp-idf
1070
1071options:
1072- name: esp_idf_version
1073 description: ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.
1074 default:
1075 - if: 'esp32c6'
1076 value: '"esp32c6"'
1077 - if: 'esp32'
1078 value: '"other"'
1079 - value: '"default"'
1080 active: true
1081"#;
1082
1083 let (cfg, options) = evaluate_yaml_config(
1084 yml,
1085 Some(esp_metadata_generated::Chip::Esp32c3),
1086 vec![],
1087 false,
1088 )
1089 .unwrap();
1090
1091 assert_eq!("esp-bootloader-esp-idf", cfg.krate);
1092
1093 assert_eq!(
1094 vec![
1095 ConfigOption {
1096 name: "esp_idf_version".to_string(),
1097 description: "ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.".to_string(),
1098 default_value: Value::String("default".to_string()),
1099 constraint: None,
1100 stability: Stability::Unstable,
1101 active: true,
1102 display_hint: DisplayHint::None,
1103 },
1104 ],
1105 options
1106 );
1107 }
1108
1109 #[test]
1110 fn deserialization_fallback_contraint() {
1111 let yml = r#"
1112crate: esp-bootloader-esp-idf
1113
1114options:
1115- name: option
1116 description: Desc
1117 default:
1118 - value: 100
1119 constraints:
1120 - if: 'esp32c6'
1121 type:
1122 validator: integer_in_range
1123 value:
1124 start: 0
1125 end: 100
1126 - if: true
1127 type:
1128 validator: integer_in_range
1129 value:
1130 start: 0
1131 end: 50
1132 active: true
1133"#;
1134
1135 let (cfg, options) = evaluate_yaml_config(
1136 yml,
1137 Some(esp_metadata_generated::Chip::Esp32),
1138 vec![],
1139 false,
1140 )
1141 .unwrap();
1142
1143 assert_eq!("esp-bootloader-esp-idf", cfg.krate);
1144
1145 assert_eq!(
1146 vec![ConfigOption {
1147 name: "option".to_string(),
1148 description: "Desc".to_string(),
1149 default_value: Value::Integer(100),
1150 constraint: Some(Validator::IntegerInRange(0..50)),
1151 stability: Stability::Unstable,
1152 active: true,
1153 display_hint: DisplayHint::None,
1154 },],
1155 options
1156 );
1157 }
1158}