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 if let Some(chip) = chip {
205 eval_ctx.add_variable("chip", chip.name());
206 eval_ctx.add_variable("ignore_feature_gates", ignore_feature_gates);
207 eval_ctx.add_function("feature", move |feature: &str| chip.contains(feature));
208 eval_ctx.add_function("cargo_feature", |feature: &str| {
209 features.contains(&feature.to_uppercase().replace("-", "_"))
210 });
211 }
212 for option in &config.options {
213 let active = eval_ctx
214 .evaluate::<bool>(&option.active)
215 .map_err(|err| Error::Parse(format!("{err:?}")))?;
216
217 let constraint = {
218 let mut active_constraint = None;
219 if let Some(constraints) = &option.constraints {
220 for constraint in constraints {
221 if eval_ctx
222 .evaluate::<bool>(&constraint.if_)
223 .map_err(|err| Error::Parse(format!("{err:?}")))?
224 {
225 active_constraint = Some(constraint.type_.clone());
226 break;
227 }
228 }
229 };
230
231 if option.constraints.is_some() && active_constraint.is_none() {
232 panic!(
233 "No constraint active for crate {}, option {}",
234 config.krate, option.name
235 );
236 }
237
238 active_constraint
239 };
240
241 let default_value = {
242 let mut default_value = None;
243 for value in &option.default {
244 if eval_ctx
245 .evaluate::<bool>(&value.if_)
246 .map_err(|err| Error::Parse(format!("{err:?}")))?
247 {
248 default_value = Some(value.value.clone());
249 break;
250 }
251 }
252
253 if default_value.is_none() {
254 panic!(
255 "No default value active for crate {}, option {}",
256 config.krate, option.name
257 );
258 }
259
260 default_value
261 };
262
263 let option = ConfigOption {
264 name: option.name.clone(),
265 description: option.description.clone(),
266 default_value: default_value.ok_or_else(|| {
267 Error::Parse(format!("No default value found for {}", option.name))
268 })?,
269 constraint,
270 stability: option.stability.clone(),
271 active,
272 display_hint: option.display_hint.unwrap_or(DisplayHint::None),
273 };
274 options.push(option);
275 }
276 Ok((config, options))
277}
278
279pub fn generate_config(
302 crate_name: &str,
303 config: &[ConfigOption],
304 enable_unstable: bool,
305 emit_md_tables: bool,
306) -> HashMap<String, Value> {
307 let configs = generate_config_internal(std::io::stdout(), crate_name, config, enable_unstable);
308
309 if emit_md_tables {
310 let file_name = snake_case(crate_name);
311
312 let mut doc_table = markdown::DOC_TABLE_HEADER.replace(
313 "{prefix}",
314 format!("{}_CONFIG_*", screaming_snake_case(crate_name)).as_str(),
315 );
316 let mut selected_config = String::from(markdown::SELECTED_TABLE_HEADER);
317
318 for (name, option, value) in configs.iter() {
319 if !option.active {
320 continue;
321 }
322 markdown::write_doc_table_line(&mut doc_table, name, option);
323 markdown::write_summary_table_line(&mut selected_config, name, value);
324 }
325
326 write_out_file(format!("{file_name}_config_table.md"), doc_table);
327 write_out_file(format!("{file_name}_selected_config.md"), selected_config);
328 }
329
330 configs.into_iter().map(|(k, _, v)| (k, v)).collect()
332}
333
334pub fn generate_config_internal<'a>(
335 mut stdout: impl Write,
336 crate_name: &str,
337 config: &'a [ConfigOption],
338 enable_unstable: bool,
339) -> Vec<(String, &'a ConfigOption, Value)> {
340 writeln!(stdout, "cargo:rerun-if-changed=build.rs").ok();
343
344 let prefix = format!("{}_CONFIG_", screaming_snake_case(crate_name));
346
347 let mut configs = create_config(&prefix, config);
348 capture_from_env(crate_name, &prefix, &mut configs, enable_unstable);
349
350 for (_, option, value) in configs.iter() {
351 if let Some(ref validator) = option.constraint {
352 validator.validate(value).unwrap();
353 }
354 }
355
356 emit_configuration(&mut stdout, &configs);
357
358 configs
359}
360
361#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
363pub enum Stability {
364 Unstable,
367 Stable(String),
370}
371
372impl Display for Stability {
373 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374 match self {
375 Stability::Unstable => write!(f, "⚠️ Unstable"),
376 Stability::Stable(version) => write!(f, "Stable since {version}"),
377 }
378 }
379}
380
381#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
383pub enum DisplayHint {
384 None,
386
387 Binary,
389
390 Hex,
392
393 Octal,
395}
396
397impl DisplayHint {
398 pub fn format_value(self, value: &Value) -> String {
400 match value {
401 Value::Bool(b) => b.to_string(),
402 Value::Integer(i) => match self {
403 DisplayHint::None => format!("{i}"),
404 DisplayHint::Binary => format!("0b{i:0b}"),
405 DisplayHint::Hex => format!("0x{i:X}"),
406 DisplayHint::Octal => format!("0o{i:o}"),
407 },
408 Value::String(s) => s.clone(),
409 }
410 }
411}
412
413#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
415pub struct ConfigOption {
416 pub name: String,
421
422 pub description: String,
427
428 pub default_value: Value,
430
431 pub constraint: Option<Validator>,
433
434 pub stability: Stability,
436
437 pub active: bool,
442
443 pub display_hint: DisplayHint,
445}
446
447impl ConfigOption {
448 pub fn full_env_var(&self, crate_name: &str) -> String {
450 self.env_var(&format!("{}_CONFIG_", screaming_snake_case(crate_name)))
451 }
452
453 fn env_var(&self, prefix: &str) -> String {
454 format!("{}{}", prefix, screaming_snake_case(&self.name))
455 }
456
457 fn cfg_name(&self) -> String {
458 snake_case(&self.name)
459 }
460
461 fn is_stable(&self) -> bool {
462 matches!(self.stability, Stability::Stable(_))
463 }
464}
465
466fn create_config<'a>(
467 prefix: &str,
468 config: &'a [ConfigOption],
469) -> Vec<(String, &'a ConfigOption, Value)> {
470 let mut configs = Vec::with_capacity(config.len());
471
472 for option in config {
473 configs.push((option.env_var(prefix), option, option.default_value.clone()));
474 }
475
476 configs
477}
478
479fn capture_from_env(
480 crate_name: &str,
481 prefix: &str,
482 configs: &mut Vec<(String, &ConfigOption, Value)>,
483 enable_unstable: bool,
484) {
485 let mut unknown = Vec::new();
486 let mut failed = Vec::new();
487 let mut unstable = Vec::new();
488
489 for (var, value) in env::vars() {
491 if var.starts_with(prefix) {
492 let Some((_, option, cfg)) = configs.iter_mut().find(|(k, _, _)| k == &var) else {
493 unknown.push(var);
494 continue;
495 };
496
497 if !option.active {
498 unknown.push(var);
499 continue;
500 }
501
502 if !enable_unstable && !option.is_stable() {
503 unstable.push(var);
504 continue;
505 }
506
507 if let Err(e) = cfg.parse_in_place(&value) {
508 failed.push(format!("{var}: {e}"));
509 }
510 }
511 }
512
513 if !failed.is_empty() {
514 panic!("Invalid configuration options detected: {failed:?}");
515 }
516
517 if !unstable.is_empty() {
518 panic!(
519 "The following configuration options are unstable: {unstable:?}. You can enable it by \
520 activating the 'unstable' feature in {crate_name}."
521 );
522 }
523
524 if !unknown.is_empty() {
525 panic!("Unknown configuration options detected: {unknown:?}");
526 }
527}
528
529fn emit_configuration(mut stdout: impl Write, configs: &[(String, &ConfigOption, Value)]) {
530 for (env_var_name, option, value) in configs.iter() {
531 let cfg_name = option.cfg_name();
532
533 writeln!(stdout, "cargo:rustc-env={env_var_name}={value}").ok();
537 writeln!(stdout, "cargo:rerun-if-env-changed={env_var_name}").ok();
538
539 writeln!(stdout, "cargo:rustc-check-cfg=cfg({cfg_name})").ok();
541
542 if let Value::Bool(true) = value {
544 writeln!(stdout, "cargo:rustc-cfg={cfg_name}").ok();
545 }
546
547 if let Some(validator) = option.constraint.as_ref() {
549 validator.emit_cargo_extras(&mut stdout, &cfg_name, value);
550 }
551 }
552}
553
554fn write_out_file(file_name: String, json: String) {
555 let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
556 let out_file = out_dir.join(file_name);
557 fs::write(out_file, json).unwrap();
558}
559
560fn snake_case(name: &str) -> String {
561 let mut name = name.replace("-", "_");
562 name.make_ascii_lowercase();
563
564 name
565}
566
567fn screaming_snake_case(name: &str) -> String {
568 let mut name = name.replace("-", "_");
569 name.make_ascii_uppercase();
570
571 name
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use crate::generate::{validator::Validator, value::Value};
578
579 #[test]
580 fn value_number_formats() {
581 const INPUTS: &[&str] = &["0xAA", "0o252", "0b0000000010101010", "170"];
582 let mut v = Value::Integer(0);
583
584 for input in INPUTS {
585 v.parse_in_place(input).unwrap();
586 assert_eq!(v.to_string(), "170");
588 }
589 }
590
591 #[test]
592 fn value_bool_inputs() {
593 let mut v = Value::Bool(false);
594
595 v.parse_in_place("true").unwrap();
596 assert_eq!(v.to_string(), "true");
597
598 v.parse_in_place("false").unwrap();
599 assert_eq!(v.to_string(), "false");
600
601 v.parse_in_place("else")
602 .expect_err("Only true or false are valid");
603 }
604
605 #[test]
606 fn env_override() {
607 temp_env::with_vars(
608 [
609 ("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
610 ("ESP_TEST_CONFIG_NUMBER_SIGNED", Some("-999")),
611 ("ESP_TEST_CONFIG_STRING", Some("Hello world!")),
612 ("ESP_TEST_CONFIG_BOOL", Some("true")),
613 ],
614 || {
615 let configs = generate_config(
616 "esp-test",
617 &[
618 ConfigOption {
619 name: String::from("number"),
620 description: String::from("NA"),
621 default_value: Value::Integer(999),
622 constraint: None,
623 stability: Stability::Stable(String::from("testing")),
624 active: true,
625 display_hint: DisplayHint::None,
626 },
627 ConfigOption {
628 name: String::from("number_signed"),
629 description: String::from("NA"),
630 default_value: Value::Integer(-777),
631 constraint: None,
632 stability: Stability::Stable(String::from("testing")),
633 active: true,
634 display_hint: DisplayHint::None,
635 },
636 ConfigOption {
637 name: String::from("string"),
638 description: String::from("NA"),
639 default_value: Value::String("Demo".to_string()),
640 constraint: None,
641 stability: Stability::Stable(String::from("testing")),
642 active: true,
643 display_hint: DisplayHint::None,
644 },
645 ConfigOption {
646 name: String::from("bool"),
647 description: String::from("NA"),
648 default_value: Value::Bool(false),
649 constraint: None,
650 stability: Stability::Stable(String::from("testing")),
651 active: true,
652 display_hint: DisplayHint::None,
653 },
654 ConfigOption {
655 name: String::from("number_default"),
656 description: String::from("NA"),
657 default_value: Value::Integer(999),
658 constraint: None,
659 stability: Stability::Stable(String::from("testing")),
660 active: true,
661 display_hint: DisplayHint::None,
662 },
663 ConfigOption {
664 name: String::from("string_default"),
665 description: String::from("NA"),
666 default_value: Value::String("Demo".to_string()),
667 constraint: None,
668 stability: Stability::Stable(String::from("testing")),
669 active: true,
670 display_hint: DisplayHint::None,
671 },
672 ConfigOption {
673 name: String::from("bool_default"),
674 description: String::from("NA"),
675 default_value: Value::Bool(false),
676 constraint: None,
677 stability: Stability::Stable(String::from("testing")),
678 active: true,
679 display_hint: DisplayHint::None,
680 },
681 ],
682 false,
683 false,
684 );
685
686 assert_eq!(configs["ESP_TEST_CONFIG_NUMBER"], Value::Integer(0xaa));
688 assert_eq!(
689 configs["ESP_TEST_CONFIG_NUMBER_SIGNED"],
690 Value::Integer(-999)
691 );
692 assert_eq!(
693 configs["ESP_TEST_CONFIG_STRING"],
694 Value::String("Hello world!".to_string())
695 );
696 assert_eq!(configs["ESP_TEST_CONFIG_BOOL"], Value::Bool(true));
697
698 assert_eq!(
700 configs["ESP_TEST_CONFIG_NUMBER_DEFAULT"],
701 Value::Integer(999)
702 );
703 assert_eq!(
704 configs["ESP_TEST_CONFIG_STRING_DEFAULT"],
705 Value::String("Demo".to_string())
706 );
707 assert_eq!(configs["ESP_TEST_CONFIG_BOOL_DEFAULT"], Value::Bool(false));
708 },
709 )
710 }
711
712 #[test]
713 fn builtin_validation_passes() {
714 temp_env::with_vars(
715 [
716 ("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("7")),
717 ("ESP_TEST_CONFIG_NEGATIVE_NUMBER", Some("-1")),
718 ("ESP_TEST_CONFIG_NON_NEGATIVE_NUMBER", Some("0")),
719 ("ESP_TEST_CONFIG_RANGE", Some("9")),
720 ],
721 || {
722 generate_config(
723 "esp-test",
724 &[
725 ConfigOption {
726 name: String::from("positive_number"),
727 description: String::from("NA"),
728 default_value: Value::Integer(-1),
729 constraint: Some(Validator::PositiveInteger),
730 stability: Stability::Stable(String::from("testing")),
731 active: true,
732 display_hint: DisplayHint::None,
733 },
734 ConfigOption {
735 name: String::from("negative_number"),
736 description: String::from("NA"),
737 default_value: Value::Integer(1),
738 constraint: Some(Validator::NegativeInteger),
739 stability: Stability::Stable(String::from("testing")),
740 active: true,
741 display_hint: DisplayHint::None,
742 },
743 ConfigOption {
744 name: String::from("non_negative_number"),
745 description: String::from("NA"),
746 default_value: Value::Integer(-1),
747 constraint: Some(Validator::NonNegativeInteger),
748 stability: Stability::Stable(String::from("testing")),
749 active: true,
750 display_hint: DisplayHint::None,
751 },
752 ConfigOption {
753 name: String::from("range"),
754 description: String::from("NA"),
755 default_value: Value::Integer(0),
756 constraint: Some(Validator::IntegerInRange(5..10)),
757 stability: Stability::Stable(String::from("testing")),
758 active: true,
759 display_hint: DisplayHint::None,
760 },
761 ],
762 false,
763 false,
764 )
765 },
766 );
767 }
768
769 #[test]
770 #[should_panic]
771 fn builtin_validation_bails() {
772 temp_env::with_vars([("ESP_TEST_CONFIG_POSITIVE_NUMBER", Some("-99"))], || {
773 generate_config(
774 "esp-test",
775 &[ConfigOption {
776 name: String::from("positive_number"),
777 description: String::from("NA"),
778 default_value: Value::Integer(-1),
779 constraint: Some(Validator::PositiveInteger),
780 stability: Stability::Stable(String::from("testing")),
781 active: true,
782 display_hint: DisplayHint::None,
783 }],
784 false,
785 false,
786 )
787 });
788 }
789
790 #[test]
791 #[should_panic]
792 fn env_unknown_bails() {
793 temp_env::with_vars(
794 [
795 ("ESP_TEST_CONFIG_NUMBER", Some("0xaa")),
796 ("ESP_TEST_CONFIG_RANDOM_VARIABLE", Some("")),
797 ],
798 || {
799 generate_config(
800 "esp-test",
801 &[ConfigOption {
802 name: String::from("number"),
803 description: String::from("NA"),
804 default_value: Value::Integer(999),
805 constraint: None,
806 stability: Stability::Stable(String::from("testing")),
807 active: true,
808 display_hint: DisplayHint::None,
809 }],
810 false,
811 false,
812 );
813 },
814 );
815 }
816
817 #[test]
818 #[should_panic]
819 fn env_invalid_values_bails() {
820 temp_env::with_vars([("ESP_TEST_CONFIG_NUMBER", Some("Hello world"))], || {
821 generate_config(
822 "esp-test",
823 &[ConfigOption {
824 name: String::from("number"),
825 description: String::from("NA"),
826 default_value: Value::Integer(999),
827 constraint: None,
828 stability: Stability::Stable(String::from("testing")),
829 active: true,
830 display_hint: DisplayHint::None,
831 }],
832 false,
833 false,
834 );
835 });
836 }
837
838 #[test]
839 fn env_unknown_prefix_is_ignored() {
840 temp_env::with_vars(
841 [("ESP_TEST_OTHER_CONFIG_NUMBER", Some("Hello world"))],
842 || {
843 generate_config(
844 "esp-test",
845 &[ConfigOption {
846 name: String::from("number"),
847 description: String::from("NA"),
848 default_value: Value::Integer(999),
849 constraint: None,
850 stability: Stability::Stable(String::from("testing")),
851 active: true,
852 display_hint: DisplayHint::None,
853 }],
854 false,
855 false,
856 );
857 },
858 );
859 }
860
861 #[test]
862 fn enumeration_validator() {
863 let mut stdout = Vec::new();
864 temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
865 generate_config_internal(
866 &mut stdout,
867 "esp-test",
868 &[ConfigOption {
869 name: String::from("some-key"),
870 description: String::from("NA"),
871 default_value: Value::String("variant-0".to_string()),
872 constraint: Some(Validator::Enumeration(vec![
873 "variant-0".to_string(),
874 "variant-1".to_string(),
875 ])),
876 stability: Stability::Stable(String::from("testing")),
877 active: true,
878 display_hint: DisplayHint::None,
879 }],
880 false,
881 );
882 });
883
884 let cargo_lines: Vec<&str> = std::str::from_utf8(&stdout).unwrap().lines().collect();
885 assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key)"));
886 assert!(cargo_lines.contains(&"cargo:rustc-env=ESP_TEST_CONFIG_SOME_KEY=variant-0"));
887 assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_0)"));
888 assert!(cargo_lines.contains(&"cargo:rustc-check-cfg=cfg(some_key_variant_1)"));
889 assert!(cargo_lines.contains(&"cargo:rustc-cfg=some_key_variant_0"));
890 }
891
892 #[test]
893 #[should_panic]
894 fn unstable_option_panics_unless_enabled() {
895 let mut stdout = Vec::new();
896 temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
897 generate_config_internal(
898 &mut stdout,
899 "esp-test",
900 &[ConfigOption {
901 name: String::from("some-key"),
902 description: String::from("NA"),
903 default_value: Value::String("variant-0".to_string()),
904 constraint: Some(Validator::Enumeration(vec![
905 "variant-0".to_string(),
906 "variant-1".to_string(),
907 ])),
908 stability: Stability::Unstable,
909 active: true,
910 display_hint: DisplayHint::None,
911 }],
912 false,
913 );
914 });
915 }
916
917 #[test]
918 #[should_panic]
919 fn inactive_option_panics() {
920 let mut stdout = Vec::new();
921 temp_env::with_vars([("ESP_TEST_CONFIG_SOME_KEY", Some("variant-0"))], || {
922 generate_config_internal(
923 &mut stdout,
924 "esp-test",
925 &[ConfigOption {
926 name: String::from("some-key"),
927 description: String::from("NA"),
928 default_value: Value::String("variant-0".to_string()),
929 constraint: Some(Validator::Enumeration(vec![
930 "variant-0".to_string(),
931 "variant-1".to_string(),
932 ])),
933 stability: Stability::Stable(String::from("testing")),
934 active: false,
935 display_hint: DisplayHint::None,
936 }],
937 false,
938 );
939 });
940 }
941
942 #[test]
943 fn deserialization() {
944 let yml = r#"
945crate: esp-bootloader-esp-idf
946
947options:
948- name: mmu_page_size
949 description: ESP32-C2, ESP32-C6 and ESP32-H2 support configurable page sizes. This is currently only used to populate the app descriptor.
950 default:
951 - value: '"64k"'
952 stability: !Stable xxxx
953 constraints:
954 - if: true
955 type:
956 validator: enumeration
957 value:
958 - 8k
959 - 16k
960 - 32k
961 - 64k
962
963- name: esp_idf_version
964 description: ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.
965 default:
966 - if: 'chip == "esp32c6"'
967 value: '"esp32c6"'
968 - if: 'chip == "esp32"'
969 value: '"other"'
970 active: true
971
972- name: partition-table-offset
973 description: "The address of partition table (by default 0x8000). Allows you to \
974 move the partition table, it gives more space for the bootloader. Note that the \
975 bootloader and app will both need to be compiled with the same \
976 PARTITION_TABLE_OFFSET value."
977 default:
978 - if: true
979 value: 32768
980 stability: Unstable
981 active: 'chip == "esp32c6"'
982"#;
983
984 let (cfg, options) = evaluate_yaml_config(
985 yml,
986 Some(esp_metadata_generated::Chip::Esp32c6),
987 vec![],
988 false,
989 )
990 .unwrap();
991
992 assert_eq!("esp-bootloader-esp-idf", cfg.krate);
993
994 assert_eq!(
995 vec![
996 ConfigOption {
997 name: "mmu_page_size".to_string(),
998 description: "ESP32-C2, ESP32-C6 and ESP32-H2 support configurable page sizes. This is currently only used to populate the app descriptor.".to_string(),
999 default_value: Value::String("64k".to_string()),
1000 constraint: Some(
1001 Validator::Enumeration(
1002 vec![
1003 "8k".to_string(),
1004 "16k".to_string(),
1005 "32k".to_string(),
1006 "64k".to_string(),
1007 ],
1008 ),
1009 ),
1010 stability: Stability::Stable("xxxx".to_string()),
1011 active: true,
1012 display_hint: DisplayHint::None,
1013 },
1014 ConfigOption {
1015 name: "esp_idf_version".to_string(),
1016 description: "ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.".to_string(),
1017 default_value: Value::String("esp32c6".to_string()),
1018 constraint: None,
1019 stability: Stability::Unstable,
1020 active: true,
1021 display_hint: DisplayHint::None,
1022 },
1023 ConfigOption {
1024 name: "partition-table-offset".to_string(),
1025 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(),
1026 default_value: Value::Integer(32768),
1027 constraint: None,
1028 stability: Stability::Unstable,
1029 active: true,
1030 display_hint: DisplayHint::None,
1031 },
1032 ],
1033 options
1034 );
1035 }
1036
1037 #[test]
1038 fn deserialization_fallback_default() {
1039 let yml = r#"
1040crate: esp-bootloader-esp-idf
1041
1042options:
1043- name: esp_idf_version
1044 description: ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.
1045 default:
1046 - if: 'chip == "esp32c6"'
1047 value: '"esp32c6"'
1048 - if: 'chip == "esp32"'
1049 value: '"other"'
1050 - value: '"default"'
1051 active: true
1052"#;
1053
1054 let (cfg, options) = evaluate_yaml_config(
1055 yml,
1056 Some(esp_metadata_generated::Chip::Esp32c3),
1057 vec![],
1058 false,
1059 )
1060 .unwrap();
1061
1062 assert_eq!("esp-bootloader-esp-idf", cfg.krate);
1063
1064 assert_eq!(
1065 vec![
1066 ConfigOption {
1067 name: "esp_idf_version".to_string(),
1068 description: "ESP-IDF version used in the application descriptor. Currently it's not checked by the bootloader.".to_string(),
1069 default_value: Value::String("default".to_string()),
1070 constraint: None,
1071 stability: Stability::Unstable,
1072 active: true,
1073 display_hint: DisplayHint::None,
1074 },
1075 ],
1076 options
1077 );
1078 }
1079
1080 #[test]
1081 fn deserialization_fallback_contraint() {
1082 let yml = r#"
1083crate: esp-bootloader-esp-idf
1084
1085options:
1086- name: option
1087 description: Desc
1088 default:
1089 - value: 100
1090 constraints:
1091 - if: 'chip == "esp32c6"'
1092 type:
1093 validator: integer_in_range
1094 value:
1095 start: 0
1096 end: 100
1097 - if: true
1098 type:
1099 validator: integer_in_range
1100 value:
1101 start: 0
1102 end: 50
1103 active: true
1104"#;
1105
1106 let (cfg, options) = evaluate_yaml_config(
1107 yml,
1108 Some(esp_metadata_generated::Chip::Esp32),
1109 vec![],
1110 false,
1111 )
1112 .unwrap();
1113
1114 assert_eq!("esp-bootloader-esp-idf", cfg.krate);
1115
1116 assert_eq!(
1117 vec![ConfigOption {
1118 name: "option".to_string(),
1119 description: "Desc".to_string(),
1120 default_value: Value::Integer(100),
1121 constraint: Some(Validator::IntegerInRange(0..50)),
1122 stability: Stability::Unstable,
1123 active: true,
1124 display_hint: DisplayHint::None,
1125 },],
1126 options
1127 );
1128 }
1129}