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}`")]
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(
556 selection: &ProfileSelection,
557 definitions: &BTreeMap<ProfileName, ProfileDefinition>,
558) -> Result<ResolvedProfile, ProfileResolutionError> {
559 validate_definitions(definitions)?;
560
561 let mut chain: Vec<ProfileName> = Vec::new();
562 let mut seen: BTreeSet<ProfileName> = BTreeSet::new();
563 let mut cursor = selection.name.clone();
564
565 loop {
570 if !seen.insert(cursor.clone()) {
571 let mut display: Vec<String> = chain.iter().map(|n| n.as_str().to_owned()).collect();
573 display.push(cursor.as_str().to_owned());
574 return Err(ProfileResolutionError::InheritanceCycle { chain: display });
575 }
576 chain.push(cursor.clone());
577
578 if let Some(def) = definitions.get(&cursor) {
579 match (cursor.as_builtin(), &def.inherits) {
580 (Some(_), None) => break,
581 (None, Some(parent)) => {
582 if !definitions.contains_key(parent) && parent.as_builtin().is_none() {
583 return Err(ProfileResolutionError::UnknownInheritedProfile {
584 profile: cursor.as_str().to_owned(),
585 parent: parent.as_str().to_owned(),
586 });
587 }
588 cursor = parent.clone();
589 continue;
590 }
591 (Some(_), Some(_)) => {
592 unreachable!("validate_definitions rejects `inherits` on built-ins")
593 }
594 (None, None) => {
595 unreachable!("validate_definitions rejects custom profiles without `inherits`")
596 }
597 }
598 }
599
600 if cursor.as_builtin().is_some() {
601 break;
602 }
603
604 return Err(ProfileResolutionError::UnknownProfile {
605 name: cursor.as_str().to_owned(),
606 });
607 }
608
609 chain.reverse();
612
613 let root_name = chain.first().expect("chain is non-empty after walk");
614 let builtin = root_name
615 .as_builtin()
616 .ok_or_else(|| ProfileResolutionError::UnknownProfile {
617 name: root_name.as_str().to_owned(),
618 })?;
619 let defaults = builtin.defaults();
620
621 let mut debug = defaults.debug;
622 let mut opt_level = defaults.opt_level;
623 let mut assertions = defaults.assertions;
624 let mut merged_build: Option<crate::build_flags::ProfileFlags> = None;
630 for step in &chain {
631 if let Some(def) = definitions.get(step) {
632 if let Some(d) = def.debug {
633 debug = d;
634 }
635 if let Some(o) = def.opt_level {
636 opt_level = o;
637 }
638 if let Some(a) = def.assertions {
639 assertions = a;
640 }
641 if let Some(layer) = def.build.as_ref() {
642 let acc =
643 merged_build.get_or_insert_with(crate::build_flags::ProfileFlags::default);
644 acc.append_layer(layer);
645 }
646 }
647 }
648
649 let final_name = selection.name.clone();
650 let source = match (
651 final_name.as_builtin(),
652 definitions.contains_key(&final_name),
653 ) {
654 (Some(_), true) => ProfileSource::BuiltinOverridden,
655 (Some(_), false) => ProfileSource::Builtin,
656 (None, _) => ProfileSource::Custom,
657 };
658
659 Ok(ResolvedProfile {
660 name: final_name,
661 debug,
662 opt_level,
663 assertions,
664 source,
665 inherits_chain: chain,
666 build: merged_build,
667 })
668}
669
670fn validate_definitions(
675 definitions: &BTreeMap<ProfileName, ProfileDefinition>,
676) -> Result<(), ProfileResolutionError> {
677 for (name, def) in definitions {
678 match (name.as_builtin(), &def.inherits) {
679 (Some(_), Some(_)) => {
680 return Err(ProfileResolutionError::BuiltinCannotInherit {
681 name: name.as_str().to_owned(),
682 });
683 }
684 (None, None) => {
685 return Err(ProfileResolutionError::CustomMissingInherits {
686 name: name.as_str().to_owned(),
687 });
688 }
689 (None, Some(parent)) => {
690 if !definitions.contains_key(parent) && parent.as_builtin().is_none() {
691 return Err(ProfileResolutionError::UnknownInheritedProfile {
692 profile: name.as_str().to_owned(),
693 parent: parent.as_str().to_owned(),
694 });
695 }
696 }
697 (Some(_), None) => {}
698 }
699 }
700 Ok(())
701}
702
703pub fn available_profile_names(
708 definitions: &BTreeMap<ProfileName, ProfileDefinition>,
709) -> Vec<ProfileName> {
710 let mut names: BTreeSet<ProfileName> = BTreeSet::new();
711 for builtin in BuiltinProfile::all() {
712 names.insert(ProfileName::builtin(builtin));
713 }
714 for k in definitions.keys() {
715 names.insert(k.clone());
716 }
717 names.into_iter().collect()
718}
719
720#[cfg(test)]
721mod tests {
722 use super::*;
723
724 fn name(s: &str) -> ProfileName {
725 ProfileName::new(s).unwrap()
726 }
727
728 fn def(
729 n: &str,
730 inherits: Option<&str>,
731 debug: Option<bool>,
732 opt: Option<OptLevel>,
733 assertions: Option<bool>,
734 ) -> (ProfileName, ProfileDefinition) {
735 let profile_name = name(n);
736 let def = ProfileDefinition {
737 name: profile_name.clone(),
738 inherits: inherits.map(name),
739 debug,
740 opt_level: opt,
741 assertions,
742 build: None,
743 };
744 (profile_name, def)
745 }
746
747 fn defs(
748 items: Vec<(ProfileName, ProfileDefinition)>,
749 ) -> BTreeMap<ProfileName, ProfileDefinition> {
750 items.into_iter().collect()
751 }
752
753 #[test]
754 fn dev_default_is_built_in_and_unmodified() {
755 let r = resolve_profile(&ProfileSelection::default_dev(), &BTreeMap::new()).unwrap();
756 assert_eq!(r.name.as_str(), "dev");
757 assert!(r.debug);
758 assert_eq!(r.opt_level, OptLevel::O0);
759 assert!(r.assertions);
760 assert_eq!(r.source, ProfileSource::Builtin);
761 assert_eq!(r.inherits_chain.len(), 1);
762 }
763
764 #[test]
765 fn release_default_is_built_in_and_unmodified() {
766 let r = resolve_profile(&ProfileSelection::release_alias(), &BTreeMap::new()).unwrap();
767 assert_eq!(r.name.as_str(), "release");
768 assert!(!r.debug);
769 assert_eq!(r.opt_level, OptLevel::O3);
770 assert!(!r.assertions);
771 assert_eq!(r.source, ProfileSource::Builtin);
772 }
773
774 #[test]
775 fn dev_override_marks_source_builtin_overridden() {
776 let d = defs(vec![def(
777 "dev",
778 None,
779 Some(false),
780 Some(OptLevel::O2),
781 None,
782 )]);
783 let r = resolve_profile(&ProfileSelection::default_dev(), &d).unwrap();
784 assert_eq!(r.opt_level, OptLevel::O2);
785 assert!(!r.debug);
786 assert!(r.assertions);
788 assert_eq!(r.source, ProfileSource::BuiltinOverridden);
789 }
790
791 #[test]
792 fn release_override_keeps_unaffected_fields() {
793 let d = defs(vec![def("release", None, Some(true), None, None)]);
794 let r = resolve_profile(&ProfileSelection::release_alias(), &d).unwrap();
795 assert!(r.debug);
796 assert_eq!(r.opt_level, OptLevel::O3);
797 assert!(!r.assertions);
798 assert_eq!(r.source, ProfileSource::BuiltinOverridden);
799 }
800
801 #[test]
802 fn custom_profile_inherits_from_release_then_overrides_debug() {
803 let d = defs(vec![def(
804 "relwithdebinfo",
805 Some("release"),
806 Some(true),
807 None,
808 None,
809 )]);
810 let r = resolve_profile(&ProfileSelection::from_name(name("relwithdebinfo")), &d).unwrap();
811 assert!(r.debug);
812 assert_eq!(r.opt_level, OptLevel::O3);
813 assert!(!r.assertions);
814 assert_eq!(r.source, ProfileSource::Custom);
815 let chain: Vec<&str> = r
816 .inherits_chain
817 .iter()
818 .map(super::ProfileName::as_str)
819 .collect();
820 assert_eq!(chain, vec!["release", "relwithdebinfo"]);
821 }
822
823 #[test]
824 fn custom_chain_through_another_custom_resolves_deterministically() {
825 let d = defs(vec![
826 def(
827 "intermediate",
828 Some("release"),
829 None,
830 Some(OptLevel::O2),
831 None,
832 ),
833 def("ci", Some("intermediate"), Some(true), None, Some(true)),
834 ]);
835 let r = resolve_profile(&ProfileSelection::from_name(name("ci")), &d).unwrap();
836 assert!(r.debug);
837 assert_eq!(r.opt_level, OptLevel::O2);
838 assert!(r.assertions);
839 let chain: Vec<&str> = r
840 .inherits_chain
841 .iter()
842 .map(super::ProfileName::as_str)
843 .collect();
844 assert_eq!(chain, vec!["release", "intermediate", "ci"]);
845 }
846
847 fn def_full(
848 n: &str,
849 inherits: Option<&str>,
850 debug: Option<bool>,
851 opt: Option<OptLevel>,
852 assertions: Option<bool>,
853 build: Option<crate::build_flags::ProfileFlags>,
854 ) -> (ProfileName, ProfileDefinition) {
855 let profile_name = name(n);
856 let def = ProfileDefinition {
857 name: profile_name.clone(),
858 inherits: inherits.map(name),
859 debug,
860 opt_level: opt,
861 assertions,
862 build,
863 };
864 (profile_name, def)
865 }
866
867 fn flags_cxx(values: &[&str]) -> crate::build_flags::ProfileFlags {
868 crate::build_flags::ProfileFlags {
869 cxxflags: values.iter().map(|s| (*s).to_owned()).collect(),
870 ..Default::default()
871 }
872 }
873
874 #[test]
875 fn cxxflags_append_across_inheritance() {
876 let d = defs(vec![
877 def_full("release", None, None, None, None, Some(flags_cxx(&["-O3"]))),
878 def_full(
879 "bench",
880 Some("release"),
881 None,
882 None,
883 None,
884 Some(flags_cxx(&["-pg"])),
885 ),
886 ]);
887 let r = resolve_profile(&ProfileSelection::from_name(name("bench")), &d).unwrap();
888 let build = r
889 .build
890 .expect("merged build is some when chain contributes");
891 assert_eq!(build.cxxflags, vec!["-O3".to_owned(), "-pg".to_owned()]);
892 }
893
894 #[test]
895 fn parent_build_inherited_when_leaf_has_no_build() {
896 let d = defs(vec![
897 def_full("release", None, None, None, None, Some(flags_cxx(&["-O3"]))),
898 def_full("bench", Some("release"), None, None, None, None),
899 ]);
900 let r = resolve_profile(&ProfileSelection::from_name(name("bench")), &d).unwrap();
901 let build = r.build.expect("parent build survives leaf having no build");
902 assert_eq!(build.cxxflags, vec!["-O3".to_owned()]);
903 }
904
905 #[test]
906 fn include_dirs_dedup_across_inheritance() {
907 use std::path::PathBuf;
908 let parent_flags = crate::build_flags::ProfileFlags {
909 include_dirs: vec![PathBuf::from("include"), PathBuf::from("vendor/include")],
910 ..Default::default()
911 };
912 let leaf_flags = crate::build_flags::ProfileFlags {
913 include_dirs: vec![PathBuf::from("include"), PathBuf::from("third_party")],
914 ..Default::default()
915 };
916 let d = defs(vec![
917 def_full("release", None, None, None, None, Some(parent_flags)),
918 def_full("bench", Some("release"), None, None, None, Some(leaf_flags)),
919 ]);
920 let r = resolve_profile(&ProfileSelection::from_name(name("bench")), &d).unwrap();
921 let build = r.build.expect("merged build is some");
922 assert_eq!(
923 build.include_dirs,
924 vec![
925 PathBuf::from("include"),
926 PathBuf::from("vendor/include"),
927 PathBuf::from("third_party"),
928 ],
929 );
930 }
931
932 #[test]
933 fn scalar_fields_replace_across_inheritance() {
934 let d = defs(vec![
935 def_full(
936 "release",
937 None,
938 Some(false),
939 Some(OptLevel::O3),
940 Some(false),
941 Some(flags_cxx(&["-O3"])),
942 ),
943 def_full(
944 "bench",
945 Some("release"),
946 Some(true),
947 Some(OptLevel::O2),
948 Some(true),
949 Some(flags_cxx(&["-pg"])),
950 ),
951 ]);
952 let r = resolve_profile(&ProfileSelection::from_name(name("bench")), &d).unwrap();
953 assert!(r.debug, "leaf debug=true replaces parent debug=false");
954 assert_eq!(r.opt_level, OptLevel::O2, "leaf opt-level replaces parent");
955 assert!(r.assertions, "leaf assertions replaces parent");
956 let build = r.build.expect("merged build is some");
957 assert_eq!(
958 build.cxxflags,
959 vec!["-O3".to_owned(), "-pg".to_owned()],
960 "arrays still append even though scalars replace",
961 );
962 }
963
964 #[test]
965 fn build_is_none_when_no_chain_step_sets_build() {
966 let d = defs(vec![
967 def_full("ci", Some("release"), Some(true), None, None, None),
968 def_full(
969 "ci-strict",
970 Some("ci"),
971 None,
972 Some(OptLevel::O2),
973 None,
974 None,
975 ),
976 ]);
977 let r = resolve_profile(&ProfileSelection::from_name(name("ci-strict")), &d).unwrap();
978 assert!(
979 r.build.is_none(),
980 "build stays None when no chain step contributed flags",
981 );
982 }
983
984 #[test]
985 fn unknown_profile_selection_errors() {
986 let err = resolve_profile(
987 &ProfileSelection::from_name(name("fastdebug")),
988 &BTreeMap::new(),
989 )
990 .unwrap_err();
991 assert!(matches!(
992 err,
993 ProfileResolutionError::UnknownProfile { ref name } if name == "fastdebug"
994 ));
995 }
996
997 #[test]
998 fn custom_without_inherits_is_rejected() {
999 let d = defs(vec![def("ci", None, Some(true), None, None)]);
1000 let err = resolve_profile(&ProfileSelection::from_name(name("ci")), &d).unwrap_err();
1001 assert!(matches!(
1002 err,
1003 ProfileResolutionError::CustomMissingInherits { ref name } if name == "ci"
1004 ));
1005 }
1006
1007 #[test]
1008 fn builtin_with_inherits_is_rejected() {
1009 let d = defs(vec![def("dev", Some("release"), None, None, None)]);
1010 let err = resolve_profile(&ProfileSelection::default_dev(), &d).unwrap_err();
1011 assert!(matches!(
1012 err,
1013 ProfileResolutionError::BuiltinCannotInherit { ref name } if name == "dev"
1014 ));
1015 }
1016
1017 #[test]
1018 fn unknown_inherited_profile_errors() {
1019 let d = defs(vec![def("ci", Some("fast"), None, None, None)]);
1020 let err = resolve_profile(&ProfileSelection::from_name(name("ci")), &d).unwrap_err();
1021 match err {
1022 ProfileResolutionError::UnknownInheritedProfile { profile, parent } => {
1023 assert_eq!(profile, "ci");
1024 assert_eq!(parent, "fast");
1025 }
1026 other => panic!("unexpected: {other:?}"),
1027 }
1028 }
1029
1030 #[test]
1031 fn inheritance_cycle_is_detected() {
1032 let d = defs(vec![
1033 def("a", Some("b"), None, None, None),
1034 def("b", Some("a"), None, None, None),
1035 ]);
1036 let err = resolve_profile(&ProfileSelection::from_name(name("a")), &d).unwrap_err();
1037 match err {
1038 ProfileResolutionError::InheritanceCycle { chain } => {
1039 assert!(chain.contains(&"a".to_owned()));
1040 assert!(chain.contains(&"b".to_owned()));
1041 }
1042 other => panic!("unexpected: {other:?}"),
1043 }
1044 }
1045
1046 #[test]
1047 fn invalid_profile_name_is_rejected_at_construction() {
1048 for bad in [
1049 ".release",
1050 "..",
1051 "",
1052 "release/x",
1053 "release\\x",
1054 "release ",
1055 "rel?",
1056 ] {
1057 assert!(ProfileName::new(bad).is_err(), "{bad:?} should be invalid");
1058 }
1059 for good in [
1060 "dev",
1061 "release",
1062 "rel-with-debug-info",
1063 "ci.fast",
1064 "0",
1065 "ci_2",
1066 ] {
1067 assert!(ProfileName::new(good).is_ok(), "{good:?} should be valid");
1068 }
1069 }
1070
1071 #[test]
1072 fn opt_level_parse_round_trips_and_rejects_unknown() {
1073 for (raw, expected) in [
1074 ("0", OptLevel::O0),
1075 ("1", OptLevel::O1),
1076 ("2", OptLevel::O2),
1077 ("3", OptLevel::O3),
1078 ("s", OptLevel::S),
1079 ("z", OptLevel::Z),
1080 ] {
1081 assert_eq!(OptLevel::parse(raw).unwrap(), expected);
1082 assert_eq!(expected.as_str(), raw);
1083 }
1084 let err = OptLevel::parse("fast").unwrap_err();
1085 assert!(err.contains("invalid opt-level"));
1086 assert!(err.contains("\"fast\""));
1087 }
1088
1089 #[test]
1090 fn compile_flags_are_deterministic_and_drop_ndebug_when_assertions_on() {
1091 let r = ResolvedProfile {
1092 name: name("dev"),
1093 debug: true,
1094 opt_level: OptLevel::O0,
1095 assertions: true,
1096 source: ProfileSource::Builtin,
1097 inherits_chain: vec![name("dev")],
1098 build: None,
1099 };
1100 assert_eq!(r.compile_flags(), vec!["-O0", "-g"]);
1101
1102 let r = ResolvedProfile {
1103 name: name("release"),
1104 debug: false,
1105 opt_level: OptLevel::O3,
1106 assertions: false,
1107 source: ProfileSource::Builtin,
1108 inherits_chain: vec![name("release")],
1109 build: None,
1110 };
1111 assert_eq!(r.compile_flags(), vec!["-O3", "-DNDEBUG"]);
1112 }
1113
1114 #[test]
1115 fn compile_flags_are_language_neutral_profile_flags() {
1116 let r = ResolvedProfile {
1117 name: name("dev"),
1118 debug: true,
1119 opt_level: OptLevel::O2,
1120 assertions: false,
1121 source: ProfileSource::Builtin,
1122 inherits_chain: vec![name("dev")],
1123 build: None,
1124 };
1125 assert_eq!(r.compile_flags(), vec!["-O2", "-g", "-DNDEBUG"]);
1126 }
1127
1128 #[test]
1129 fn available_profile_names_includes_built_ins_and_custom() {
1130 let d = defs(vec![def("ci", Some("release"), None, None, None)]);
1131 let names: Vec<String> = available_profile_names(&d)
1132 .into_iter()
1133 .map(|n| n.as_str().to_owned())
1134 .collect();
1135 assert_eq!(
1136 names,
1137 vec!["ci".to_owned(), "dev".to_owned(), "release".to_owned()]
1138 );
1139 }
1140}