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