1use std::borrow::Borrow;
24use std::collections::{BTreeMap, BTreeSet};
25use std::fmt;
26
27use serde::{Deserialize, Serialize};
28use thiserror::Error;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub enum BuiltinProfile {
34 Dev,
35 Release,
36}
37
38impl BuiltinProfile {
39 pub fn all() -> [BuiltinProfile; 2] {
41 [BuiltinProfile::Dev, BuiltinProfile::Release]
42 }
43
44 pub fn as_str(self) -> &'static str {
47 match self {
48 BuiltinProfile::Dev => "dev",
49 BuiltinProfile::Release => "release",
50 }
51 }
52
53 pub fn defaults(self) -> ProfileDefaults {
55 match self {
56 BuiltinProfile::Dev => ProfileDefaults {
57 debug: true,
58 opt_level: OptLevel::O0,
59 assertions: true,
60 },
61 BuiltinProfile::Release => ProfileDefaults {
62 debug: false,
63 opt_level: OptLevel::O3,
64 assertions: false,
65 },
66 }
67 }
68
69 pub fn from_name(name: &str) -> Option<Self> {
71 match name {
72 "dev" => Some(BuiltinProfile::Dev),
73 "release" => Some(BuiltinProfile::Release),
74 _ => None,
75 }
76 }
77}
78
79impl fmt::Display for BuiltinProfile {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 f.write_str(self.as_str())
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct ProfileDefaults {
89 pub debug: bool,
90 pub opt_level: OptLevel,
91 pub assertions: bool,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
100pub enum OptLevel {
101 O0,
103 O1,
105 O2,
107 O3,
109 S,
111 Z,
114}
115
116impl OptLevel {
117 pub fn as_flag(self) -> &'static str {
121 match self {
122 OptLevel::O0 => "-O0",
123 OptLevel::O1 => "-O1",
124 OptLevel::O2 => "-O2",
125 OptLevel::O3 => "-O3",
126 OptLevel::S => "-Os",
127 OptLevel::Z => "-Oz",
128 }
129 }
130
131 pub fn as_str(self) -> &'static str {
134 match self {
135 OptLevel::O0 => "0",
136 OptLevel::O1 => "1",
137 OptLevel::O2 => "2",
138 OptLevel::O3 => "3",
139 OptLevel::S => "s",
140 OptLevel::Z => "z",
141 }
142 }
143}
144
145impl fmt::Display for OptLevel {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 f.write_str(self.as_str())
148 }
149}
150
151impl Serialize for OptLevel {
152 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
153 ser.serialize_str(self.as_str())
154 }
155}
156
157impl<'de> Deserialize<'de> for OptLevel {
158 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
159 struct V;
164 impl serde::de::Visitor<'_> for V {
165 type Value = OptLevel;
166 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 f.write_str("0, 1, 2, 3, \"s\", or \"z\"")
168 }
169 fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<OptLevel, E> {
170 OptLevel::parse(s).map_err(serde::de::Error::custom)
171 }
172 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<OptLevel, E> {
173 OptLevel::parse(&v.to_string()).map_err(serde::de::Error::custom)
174 }
175 fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<OptLevel, E> {
176 OptLevel::parse(&v.to_string()).map_err(serde::de::Error::custom)
177 }
178 }
179 de.deserialize_any(V)
180 }
181}
182
183impl OptLevel {
184 pub fn parse(raw: &str) -> Result<Self, String> {
192 match raw {
193 "0" => Ok(OptLevel::O0),
194 "1" => Ok(OptLevel::O1),
195 "2" => Ok(OptLevel::O2),
196 "3" => Ok(OptLevel::O3),
197 "s" => Ok(OptLevel::S),
198 "z" => Ok(OptLevel::Z),
199 other => Err(format!(
200 "invalid opt-level {other:?}; expected 0, 1, 2, 3, \"s\", or \"z\""
201 )),
202 }
203 }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
215#[serde(try_from = "String", into = "String")]
216pub struct ProfileName(String);
217
218impl ProfileName {
219 pub fn new(value: impl Into<String>) -> Result<Self, InvalidProfileName> {
232 let value = value.into();
233 if !is_path_safe_profile_name(&value) {
234 return Err(InvalidProfileName(value));
235 }
236 Ok(Self(value))
237 }
238
239 pub fn builtin(profile: BuiltinProfile) -> Self {
242 Self(profile.as_str().to_owned())
243 }
244
245 pub fn as_str(&self) -> &str {
246 &self.0
247 }
248
249 pub fn as_builtin(&self) -> Option<BuiltinProfile> {
252 BuiltinProfile::from_name(&self.0)
253 }
254}
255
256impl fmt::Display for ProfileName {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 f.write_str(&self.0)
259 }
260}
261
262impl AsRef<str> for ProfileName {
263 fn as_ref(&self) -> &str {
264 &self.0
265 }
266}
267
268impl Borrow<str> for ProfileName {
269 fn borrow(&self) -> &str {
270 &self.0
271 }
272}
273
274impl From<ProfileName> for String {
275 fn from(name: ProfileName) -> Self {
276 name.0
277 }
278}
279
280impl TryFrom<String> for ProfileName {
281 type Error = InvalidProfileName;
282 fn try_from(value: String) -> Result<Self, Self::Error> {
283 ProfileName::new(value)
284 }
285}
286
287pub(crate) fn is_path_safe_profile_name(name: &str) -> bool {
289 if name.is_empty() {
290 return false;
291 }
292 if name == "." || name == ".." {
293 return false;
294 }
295 if name.starts_with('.') {
296 return false;
297 }
298 name.bytes()
299 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.'))
300}
301
302#[derive(Debug, Error, Clone, PartialEq, Eq)]
307#[error(
308 "invalid profile name {0:?}; profile names must be non-empty, must not start with `.`, must not be `.` or `..`, and may only contain ASCII alphanumerics, `_`, `-`, or `.`"
309)]
310pub struct InvalidProfileName(pub String);
311
312#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
318pub struct ProfileDefinition {
319 pub name: ProfileName,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub inherits: Option<ProfileName>,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub debug: Option<bool>,
326 #[serde(default, rename = "opt-level", skip_serializing_if = "Option::is_none")]
327 pub opt_level: Option<OptLevel>,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub assertions: Option<bool>,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub build: Option<crate::build_flags::ProfileFlags>,
336}
337
338#[derive(Debug, Clone, PartialEq, Eq)]
342pub struct ProfileSelection {
343 pub name: ProfileName,
344}
345
346impl ProfileSelection {
347 pub fn default_dev() -> Self {
350 Self {
351 name: ProfileName::builtin(BuiltinProfile::Dev),
352 }
353 }
354
355 pub fn release_alias() -> Self {
358 Self {
359 name: ProfileName::builtin(BuiltinProfile::Release),
360 }
361 }
362
363 pub fn from_name(name: ProfileName) -> Self {
365 Self { name }
366 }
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
371#[serde(rename_all = "kebab-case")]
372pub enum ProfileSource {
373 Builtin,
375 BuiltinOverridden,
378 Custom,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
390pub struct ResolvedProfile {
391 pub name: ProfileName,
392 pub debug: bool,
393 pub opt_level: OptLevel,
394 pub assertions: bool,
395 pub source: ProfileSource,
396 pub inherits_chain: Vec<ProfileName>,
402 #[serde(skip)]
421 pub build: Option<crate::build_flags::ProfileFlags>,
422}
423
424impl ResolvedProfile {
425 pub fn as_json(&self) -> serde_json::Value {
429 serde_json::json!({
430 "name": self.name.as_str(),
431 "debug": self.debug,
432 "opt_level": self.opt_level.as_str(),
433 "assertions": self.assertions,
434 "source": match self.source {
435 ProfileSource::Builtin => "builtin",
436 ProfileSource::BuiltinOverridden => "builtin-overridden",
437 ProfileSource::Custom => "custom",
438 },
439 "inherits_chain": self
440 .inherits_chain
441 .iter()
442 .map(ProfileName::as_str)
443 .collect::<Vec<_>>(),
444 })
445 }
446
447 pub fn compile_flags(&self) -> Vec<&'static str> {
454 let mut out = Vec::with_capacity(3);
455 out.push(self.opt_level.as_flag());
456 if self.debug {
457 out.push("-g");
458 }
459 if !self.assertions {
460 out.push("-DNDEBUG");
461 }
462 out
463 }
464}
465
466#[derive(Debug, Error, Clone, PartialEq, Eq)]
468pub enum ProfileResolutionError {
469 #[error("unknown profile `{name}`; define it with `[profile.{name}]` and an `inherits` field")]
472 UnknownProfile { name: String },
473
474 #[error("profile `{profile}` inherits from unknown profile `{parent}`")]
477 UnknownInheritedProfile { profile: String, parent: String },
478
479 #[error("profile inheritance cycle detected: {}", display_chain(.chain))]
483 InheritanceCycle { chain: Vec<String> },
484
485 #[error("built-in profile `{name}` cannot declare `inherits`; only custom profiles inherit")]
489 BuiltinCannotInherit { name: String },
490
491 #[error(
495 "custom profile `{name}` must declare `inherits = \"dev\"` or `inherits = \"release\"` (or another custom profile)"
496 )]
497 CustomMissingInherits { name: String },
498}
499
500fn display_chain(chain: &[String]) -> String {
501 chain.join(" -> ")
502}
503
504pub fn resolve_profile(
555 selection: &ProfileSelection,
556 definitions: &BTreeMap<ProfileName, ProfileDefinition>,
557) -> Result<ResolvedProfile, ProfileResolutionError> {
558 validate_definitions(definitions)?;
559
560 let mut chain: Vec<ProfileName> = Vec::new();
561 let mut seen: BTreeSet<ProfileName> = BTreeSet::new();
562 let mut cursor = selection.name.clone();
563
564 loop {
569 if !seen.insert(cursor.clone()) {
570 let mut display: Vec<String> = chain.iter().map(|n| n.as_str().to_owned()).collect();
572 display.push(cursor.as_str().to_owned());
573 return Err(ProfileResolutionError::InheritanceCycle { chain: display });
574 }
575 chain.push(cursor.clone());
576
577 if let Some(def) = definitions.get(&cursor) {
578 match (cursor.as_builtin(), &def.inherits) {
579 (Some(_), None) => break,
580 (None, Some(parent)) => {
581 if !definitions.contains_key(parent) && parent.as_builtin().is_none() {
582 return Err(ProfileResolutionError::UnknownInheritedProfile {
583 profile: cursor.as_str().to_owned(),
584 parent: parent.as_str().to_owned(),
585 });
586 }
587 cursor = parent.clone();
588 continue;
589 }
590 (Some(_), Some(_)) => {
591 unreachable!("validate_definitions rejects `inherits` on built-ins")
592 }
593 (None, None) => {
594 unreachable!("validate_definitions rejects custom profiles without `inherits`")
595 }
596 }
597 }
598
599 if cursor.as_builtin().is_some() {
600 break;
601 }
602
603 return Err(ProfileResolutionError::UnknownProfile {
604 name: cursor.as_str().to_owned(),
605 });
606 }
607
608 chain.reverse();
611
612 let root_name = chain.first().expect("chain is non-empty after walk");
613 let builtin = root_name
614 .as_builtin()
615 .ok_or_else(|| ProfileResolutionError::UnknownProfile {
616 name: root_name.as_str().to_owned(),
617 })?;
618 let defaults = builtin.defaults();
619
620 let mut debug = defaults.debug;
621 let mut opt_level = defaults.opt_level;
622 let mut assertions = defaults.assertions;
623 let mut merged_build: Option<crate::build_flags::ProfileFlags> = None;
629 for step in &chain {
630 if let Some(def) = definitions.get(step) {
631 if let Some(d) = def.debug {
632 debug = d;
633 }
634 if let Some(o) = def.opt_level {
635 opt_level = o;
636 }
637 if let Some(a) = def.assertions {
638 assertions = a;
639 }
640 if let Some(layer) = def.build.as_ref() {
641 let acc =
642 merged_build.get_or_insert_with(crate::build_flags::ProfileFlags::default);
643 acc.append_layer(layer);
644 }
645 }
646 }
647
648 let final_name = selection.name.clone();
649 let source = match (
650 final_name.as_builtin(),
651 definitions.contains_key(&final_name),
652 ) {
653 (Some(_), true) => ProfileSource::BuiltinOverridden,
654 (Some(_), false) => ProfileSource::Builtin,
655 (None, _) => ProfileSource::Custom,
656 };
657
658 Ok(ResolvedProfile {
659 name: final_name,
660 debug,
661 opt_level,
662 assertions,
663 source,
664 inherits_chain: chain,
665 build: merged_build,
666 })
667}
668
669fn validate_definitions(
674 definitions: &BTreeMap<ProfileName, ProfileDefinition>,
675) -> Result<(), ProfileResolutionError> {
676 for (name, def) in definitions {
677 match (name.as_builtin(), &def.inherits) {
678 (Some(_), Some(_)) => {
679 return Err(ProfileResolutionError::BuiltinCannotInherit {
680 name: name.as_str().to_owned(),
681 });
682 }
683 (None, None) => {
684 return Err(ProfileResolutionError::CustomMissingInherits {
685 name: name.as_str().to_owned(),
686 });
687 }
688 (None, Some(parent)) => {
689 if !definitions.contains_key(parent) && parent.as_builtin().is_none() {
690 return Err(ProfileResolutionError::UnknownInheritedProfile {
691 profile: name.as_str().to_owned(),
692 parent: parent.as_str().to_owned(),
693 });
694 }
695 }
696 (Some(_), None) => {}
697 }
698 }
699 Ok(())
700}
701
702pub fn available_profile_names(
707 definitions: &BTreeMap<ProfileName, ProfileDefinition>,
708) -> Vec<ProfileName> {
709 let mut names: BTreeSet<ProfileName> = BTreeSet::new();
710 for builtin in BuiltinProfile::all() {
711 names.insert(ProfileName::builtin(builtin));
712 }
713 for k in definitions.keys() {
714 names.insert(k.clone());
715 }
716 names.into_iter().collect()
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 fn name(s: &str) -> ProfileName {
724 ProfileName::new(s).unwrap()
725 }
726
727 fn def(
728 n: &str,
729 inherits: Option<&str>,
730 debug: Option<bool>,
731 opt: Option<OptLevel>,
732 assertions: Option<bool>,
733 ) -> (ProfileName, ProfileDefinition) {
734 let profile_name = name(n);
735 let def = ProfileDefinition {
736 name: profile_name.clone(),
737 inherits: inherits.map(name),
738 debug,
739 opt_level: opt,
740 assertions,
741 build: None,
742 };
743 (profile_name, def)
744 }
745
746 fn defs(
747 items: Vec<(ProfileName, ProfileDefinition)>,
748 ) -> BTreeMap<ProfileName, ProfileDefinition> {
749 items.into_iter().collect()
750 }
751
752 #[test]
753 fn dev_default_is_built_in_and_unmodified() {
754 let r = resolve_profile(&ProfileSelection::default_dev(), &BTreeMap::new()).unwrap();
755 assert_eq!(r.name.as_str(), "dev");
756 assert!(r.debug);
757 assert_eq!(r.opt_level, OptLevel::O0);
758 assert!(r.assertions);
759 assert_eq!(r.source, ProfileSource::Builtin);
760 assert_eq!(r.inherits_chain.len(), 1);
761 }
762
763 #[test]
764 fn release_default_is_built_in_and_unmodified() {
765 let r = resolve_profile(&ProfileSelection::release_alias(), &BTreeMap::new()).unwrap();
766 assert_eq!(r.name.as_str(), "release");
767 assert!(!r.debug);
768 assert_eq!(r.opt_level, OptLevel::O3);
769 assert!(!r.assertions);
770 assert_eq!(r.source, ProfileSource::Builtin);
771 }
772
773 #[test]
774 fn dev_override_marks_source_builtin_overridden() {
775 let d = defs(vec![def(
776 "dev",
777 None,
778 Some(false),
779 Some(OptLevel::O2),
780 None,
781 )]);
782 let r = resolve_profile(&ProfileSelection::default_dev(), &d).unwrap();
783 assert_eq!(r.opt_level, OptLevel::O2);
784 assert!(!r.debug);
785 assert!(r.assertions);
787 assert_eq!(r.source, ProfileSource::BuiltinOverridden);
788 }
789
790 #[test]
791 fn release_override_keeps_unaffected_fields() {
792 let d = defs(vec![def("release", None, Some(true), None, None)]);
793 let r = resolve_profile(&ProfileSelection::release_alias(), &d).unwrap();
794 assert!(r.debug);
795 assert_eq!(r.opt_level, OptLevel::O3);
796 assert!(!r.assertions);
797 assert_eq!(r.source, ProfileSource::BuiltinOverridden);
798 }
799
800 #[test]
801 fn custom_profile_inherits_from_release_then_overrides_debug() {
802 let d = defs(vec![def(
803 "relwithdebinfo",
804 Some("release"),
805 Some(true),
806 None,
807 None,
808 )]);
809 let r = resolve_profile(&ProfileSelection::from_name(name("relwithdebinfo")), &d).unwrap();
810 assert!(r.debug);
811 assert_eq!(r.opt_level, OptLevel::O3);
812 assert!(!r.assertions);
813 assert_eq!(r.source, ProfileSource::Custom);
814 let chain: Vec<&str> = r
815 .inherits_chain
816 .iter()
817 .map(super::ProfileName::as_str)
818 .collect();
819 assert_eq!(chain, vec!["release", "relwithdebinfo"]);
820 }
821
822 #[test]
823 fn custom_chain_through_another_custom_resolves_deterministically() {
824 let d = defs(vec![
825 def(
826 "intermediate",
827 Some("release"),
828 None,
829 Some(OptLevel::O2),
830 None,
831 ),
832 def("ci", Some("intermediate"), Some(true), None, Some(true)),
833 ]);
834 let r = resolve_profile(&ProfileSelection::from_name(name("ci")), &d).unwrap();
835 assert!(r.debug);
836 assert_eq!(r.opt_level, OptLevel::O2);
837 assert!(r.assertions);
838 let chain: Vec<&str> = r
839 .inherits_chain
840 .iter()
841 .map(super::ProfileName::as_str)
842 .collect();
843 assert_eq!(chain, vec!["release", "intermediate", "ci"]);
844 }
845
846 fn def_full(
847 n: &str,
848 inherits: Option<&str>,
849 debug: Option<bool>,
850 opt: Option<OptLevel>,
851 assertions: Option<bool>,
852 build: Option<crate::build_flags::ProfileFlags>,
853 ) -> (ProfileName, ProfileDefinition) {
854 let profile_name = name(n);
855 let def = ProfileDefinition {
856 name: profile_name.clone(),
857 inherits: inherits.map(name),
858 debug,
859 opt_level: opt,
860 assertions,
861 build,
862 };
863 (profile_name, def)
864 }
865
866 fn flags_cxx(values: &[&str]) -> crate::build_flags::ProfileFlags {
867 crate::build_flags::ProfileFlags {
868 cxxflags: values.iter().map(|s| (*s).to_owned()).collect(),
869 ..Default::default()
870 }
871 }
872
873 #[test]
874 fn cxxflags_append_across_inheritance() {
875 let d = defs(vec![
876 def_full("release", None, None, None, None, Some(flags_cxx(&["-O3"]))),
877 def_full(
878 "bench",
879 Some("release"),
880 None,
881 None,
882 None,
883 Some(flags_cxx(&["-pg"])),
884 ),
885 ]);
886 let r = resolve_profile(&ProfileSelection::from_name(name("bench")), &d).unwrap();
887 let build = r
888 .build
889 .expect("merged build is some when chain contributes");
890 assert_eq!(build.cxxflags, vec!["-O3".to_owned(), "-pg".to_owned()]);
891 }
892
893 #[test]
894 fn parent_build_inherited_when_leaf_has_no_build() {
895 let d = defs(vec![
896 def_full("release", None, None, None, None, Some(flags_cxx(&["-O3"]))),
897 def_full("bench", Some("release"), None, None, None, None),
898 ]);
899 let r = resolve_profile(&ProfileSelection::from_name(name("bench")), &d).unwrap();
900 let build = r.build.expect("parent build survives leaf having no build");
901 assert_eq!(build.cxxflags, vec!["-O3".to_owned()]);
902 }
903
904 #[test]
905 fn include_dirs_dedup_across_inheritance() {
906 use camino::Utf8PathBuf;
907 let parent_flags = crate::build_flags::ProfileFlags {
908 include_dirs: vec![
909 Utf8PathBuf::from("include"),
910 Utf8PathBuf::from("vendor/include"),
911 ],
912 ..Default::default()
913 };
914 let leaf_flags = crate::build_flags::ProfileFlags {
915 include_dirs: vec![
916 Utf8PathBuf::from("include"),
917 Utf8PathBuf::from("third_party"),
918 ],
919 ..Default::default()
920 };
921 let d = defs(vec![
922 def_full("release", None, None, None, None, Some(parent_flags)),
923 def_full("bench", Some("release"), None, None, None, Some(leaf_flags)),
924 ]);
925 let r = resolve_profile(&ProfileSelection::from_name(name("bench")), &d).unwrap();
926 let build = r.build.expect("merged build is some");
927 assert_eq!(
928 build.include_dirs,
929 vec![
930 Utf8PathBuf::from("include"),
931 Utf8PathBuf::from("vendor/include"),
932 Utf8PathBuf::from("third_party"),
933 ],
934 );
935 }
936
937 #[test]
938 fn scalar_fields_replace_across_inheritance() {
939 let d = defs(vec![
940 def_full(
941 "release",
942 None,
943 Some(false),
944 Some(OptLevel::O3),
945 Some(false),
946 Some(flags_cxx(&["-O3"])),
947 ),
948 def_full(
949 "bench",
950 Some("release"),
951 Some(true),
952 Some(OptLevel::O2),
953 Some(true),
954 Some(flags_cxx(&["-pg"])),
955 ),
956 ]);
957 let r = resolve_profile(&ProfileSelection::from_name(name("bench")), &d).unwrap();
958 assert!(r.debug, "leaf debug=true replaces parent debug=false");
959 assert_eq!(r.opt_level, OptLevel::O2, "leaf opt-level replaces parent");
960 assert!(r.assertions, "leaf assertions replaces parent");
961 let build = r.build.expect("merged build is some");
962 assert_eq!(
963 build.cxxflags,
964 vec!["-O3".to_owned(), "-pg".to_owned()],
965 "arrays still append even though scalars replace",
966 );
967 }
968
969 #[test]
970 fn build_is_none_when_no_chain_step_sets_build() {
971 let d = defs(vec![
972 def_full("ci", Some("release"), Some(true), None, None, None),
973 def_full(
974 "ci-strict",
975 Some("ci"),
976 None,
977 Some(OptLevel::O2),
978 None,
979 None,
980 ),
981 ]);
982 let r = resolve_profile(&ProfileSelection::from_name(name("ci-strict")), &d).unwrap();
983 assert!(
984 r.build.is_none(),
985 "build stays None when no chain step contributed flags",
986 );
987 }
988
989 #[test]
990 fn unknown_profile_selection_errors() {
991 let err = resolve_profile(
992 &ProfileSelection::from_name(name("fastdebug")),
993 &BTreeMap::new(),
994 )
995 .unwrap_err();
996 assert!(matches!(
997 err,
998 ProfileResolutionError::UnknownProfile { ref name } if name == "fastdebug"
999 ));
1000 assert_eq!(
1001 err.to_string(),
1002 "unknown profile `fastdebug`; define it with `[profile.fastdebug]` and an `inherits` field",
1003 );
1004 }
1005
1006 #[test]
1007 fn custom_without_inherits_is_rejected() {
1008 let d = defs(vec![def("ci", None, Some(true), None, None)]);
1009 let err = resolve_profile(&ProfileSelection::from_name(name("ci")), &d).unwrap_err();
1010 assert!(matches!(
1011 err,
1012 ProfileResolutionError::CustomMissingInherits { ref name } if name == "ci"
1013 ));
1014 }
1015
1016 #[test]
1017 fn builtin_with_inherits_is_rejected() {
1018 let d = defs(vec![def("dev", Some("release"), None, None, None)]);
1019 let err = resolve_profile(&ProfileSelection::default_dev(), &d).unwrap_err();
1020 assert!(matches!(
1021 err,
1022 ProfileResolutionError::BuiltinCannotInherit { ref name } if name == "dev"
1023 ));
1024 }
1025
1026 #[test]
1027 fn unknown_inherited_profile_errors() {
1028 let d = defs(vec![def("ci", Some("fast"), None, None, None)]);
1029 let err = resolve_profile(&ProfileSelection::from_name(name("ci")), &d).unwrap_err();
1030 match err {
1031 ProfileResolutionError::UnknownInheritedProfile { profile, parent } => {
1032 assert_eq!(profile, "ci");
1033 assert_eq!(parent, "fast");
1034 }
1035 other => panic!("unexpected: {other:?}"),
1036 }
1037 }
1038
1039 #[test]
1040 fn inheritance_cycle_is_detected() {
1041 let d = defs(vec![
1042 def("a", Some("b"), None, None, None),
1043 def("b", Some("a"), None, None, None),
1044 ]);
1045 let err = resolve_profile(&ProfileSelection::from_name(name("a")), &d).unwrap_err();
1046 match err {
1047 ProfileResolutionError::InheritanceCycle { chain } => {
1048 assert!(chain.contains(&"a".to_owned()));
1049 assert!(chain.contains(&"b".to_owned()));
1050 }
1051 other => panic!("unexpected: {other:?}"),
1052 }
1053 }
1054
1055 #[test]
1056 fn invalid_profile_name_is_rejected_at_construction() {
1057 for bad in [
1058 ".release",
1059 "..",
1060 "",
1061 "release/x",
1062 "release\\x",
1063 "release ",
1064 "rel?",
1065 ] {
1066 assert!(ProfileName::new(bad).is_err(), "{bad:?} should be invalid");
1067 }
1068 for good in [
1069 "dev",
1070 "release",
1071 "rel-with-debug-info",
1072 "ci.fast",
1073 "0",
1074 "ci_2",
1075 ] {
1076 assert!(ProfileName::new(good).is_ok(), "{good:?} should be valid");
1077 }
1078 }
1079
1080 #[test]
1081 fn opt_level_parse_round_trips_and_rejects_unknown() {
1082 for (raw, expected) in [
1083 ("0", OptLevel::O0),
1084 ("1", OptLevel::O1),
1085 ("2", OptLevel::O2),
1086 ("3", OptLevel::O3),
1087 ("s", OptLevel::S),
1088 ("z", OptLevel::Z),
1089 ] {
1090 assert_eq!(OptLevel::parse(raw).unwrap(), expected);
1091 assert_eq!(expected.as_str(), raw);
1092 }
1093 let err = OptLevel::parse("fast").unwrap_err();
1094 assert!(err.contains("invalid opt-level"));
1095 assert!(err.contains("\"fast\""));
1096 }
1097
1098 #[test]
1099 fn compile_flags_are_deterministic_and_drop_ndebug_when_assertions_on() {
1100 let r = ResolvedProfile {
1101 name: name("dev"),
1102 debug: true,
1103 opt_level: OptLevel::O0,
1104 assertions: true,
1105 source: ProfileSource::Builtin,
1106 inherits_chain: vec![name("dev")],
1107 build: None,
1108 };
1109 assert_eq!(r.compile_flags(), vec!["-O0", "-g"]);
1110
1111 let r = ResolvedProfile {
1112 name: name("release"),
1113 debug: false,
1114 opt_level: OptLevel::O3,
1115 assertions: false,
1116 source: ProfileSource::Builtin,
1117 inherits_chain: vec![name("release")],
1118 build: None,
1119 };
1120 assert_eq!(r.compile_flags(), vec!["-O3", "-DNDEBUG"]);
1121 }
1122
1123 #[test]
1124 fn compile_flags_are_language_neutral_profile_flags() {
1125 let r = ResolvedProfile {
1126 name: name("dev"),
1127 debug: true,
1128 opt_level: OptLevel::O2,
1129 assertions: false,
1130 source: ProfileSource::Builtin,
1131 inherits_chain: vec![name("dev")],
1132 build: None,
1133 };
1134 assert_eq!(r.compile_flags(), vec!["-O2", "-g", "-DNDEBUG"]);
1135 }
1136
1137 #[test]
1138 fn available_profile_names_includes_built_ins_and_custom() {
1139 let d = defs(vec![def("ci", Some("release"), None, None, None)]);
1140 let names: Vec<String> = available_profile_names(&d)
1141 .into_iter()
1142 .map(|n| n.as_str().to_owned())
1143 .collect();
1144 assert_eq!(
1145 names,
1146 vec!["ci".to_owned(), "dev".to_owned(), "release".to_owned()]
1147 );
1148 }
1149}