alpm_types/relation/base.rs
1//! Basic relation types used in metadata files.
2
3use std::{
4 fmt::{Display, Formatter},
5 str::FromStr,
6};
7
8use alpm_parsers::traits::{AlpmParser, ParserUntil, ParserUntilInclusive};
9use serde::{Deserialize, Serialize};
10use winnow::{
11 ModalResult,
12 Parser,
13 ascii::space1,
14 combinator::{opt, peek, seq, terminated},
15 error::{StrContext, StrContextValue},
16 token::{none_of, take_till},
17};
18
19use crate::{
20 Epoch,
21 Error,
22 Name,
23 PackageRelease,
24 PackageVersion,
25 Version,
26 VersionComparison,
27 VersionRequirement,
28};
29
30/// A package relation
31///
32/// Describes a relation to a component.
33/// Package relations may either consist of only a [`Name`] *or* of a [`Name`] and a
34/// [`VersionRequirement`].
35///
36/// ## Note
37///
38/// A [`PackageRelation`] covers all [alpm-package-relations] *except* optional
39/// dependencies, as those behave differently.
40///
41/// [alpm-package-relations]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
42#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
43pub struct PackageRelation {
44 /// The name of the package
45 pub name: Name,
46 /// The version requirement for the package
47 pub version_requirement: Option<VersionRequirement>,
48}
49
50impl PackageRelation {
51 /// Creates a new [`PackageRelation`]
52 ///
53 /// # Examples
54 ///
55 /// ```
56 /// use alpm_types::{PackageRelation, VersionComparison, VersionRequirement};
57 ///
58 /// # fn main() -> Result<(), alpm_types::Error> {
59 /// PackageRelation::new(
60 /// "example".parse()?,
61 /// Some(VersionRequirement {
62 /// comparison: VersionComparison::Less,
63 /// version: "1.0.0".parse()?,
64 /// }),
65 /// );
66 ///
67 /// PackageRelation::new("example".parse()?, None);
68 /// # Ok(())
69 /// # }
70 /// ```
71 pub fn new(name: Name, version_requirement: Option<VersionRequirement>) -> Self {
72 Self {
73 name,
74 version_requirement,
75 }
76 }
77}
78
79impl AlpmParser for PackageRelation {
80 /// Recognizes a [`PackageRelation`] in a string slice.
81 ///
82 /// # Examples
83 ///
84 /// See [`Self::from_str`] for code examples.
85 ///
86 /// # Errors
87 ///
88 /// Returns an error if `input` does not begin with a valid [alpm-package-relation].
89 ///
90 /// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
91 fn parser(input: &mut &str) -> ModalResult<Self> {
92 seq!(Self {
93 name: Name::parser.context(StrContext::Label("package name")),
94 version_requirement: opt(VersionRequirement::parser),
95 })
96 .parse_next(input)
97 }
98
99 fn delimiter_error_context<'a, O, P>(
100 parser: P,
101 ) -> impl Parser<&'a str, O, winnow::error::ErrMode<winnow::error::ContextError>>
102 where
103 P: Parser<&'a str, O, winnow::error::ErrMode<winnow::error::ContextError>>,
104 {
105 parser
106 .context(StrContext::Label("alpm-package-relation"))
107 .context(StrContext::Expected(StrContextValue::Description(
108 "end of input after version requirement",
109 )))
110 }
111}
112
113impl Display for PackageRelation {
114 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
115 if let Some(version_requirement) = self.version_requirement.as_ref() {
116 write!(f, "{}{}", self.name, version_requirement)
117 } else {
118 write!(f, "{}", self.name)
119 }
120 }
121}
122
123impl FromStr for PackageRelation {
124 type Err = Error;
125 /// Parses a [`PackageRelation`] from a string slice.
126 ///
127 /// Delegates to [`PackageRelation::parser`].
128 ///
129 /// # Errors
130 ///
131 /// Returns an error if [`PackageRelation::parser`] fails.
132 ///
133 /// # Examples
134 ///
135 /// ```
136 /// use std::str::FromStr;
137 ///
138 /// use alpm_types::{PackageRelation, VersionComparison, VersionRequirement};
139 ///
140 /// # fn main() -> Result<(), alpm_types::Error> {
141 /// assert_eq!(
142 /// PackageRelation::from_str("example<1.0.0")?,
143 /// PackageRelation::new(
144 /// "example".parse()?,
145 /// Some(VersionRequirement {
146 /// comparison: VersionComparison::Less,
147 /// version: "1.0.0".parse()?
148 /// })
149 /// ),
150 /// );
151 ///
152 /// assert_eq!(
153 /// PackageRelation::from_str("example<=1.0.0")?,
154 /// PackageRelation::new(
155 /// "example".parse()?,
156 /// Some(VersionRequirement {
157 /// comparison: VersionComparison::LessOrEqual,
158 /// version: "1.0.0".parse()?
159 /// })
160 /// ),
161 /// );
162 ///
163 /// assert_eq!(
164 /// PackageRelation::from_str("example=1.0.0")?,
165 /// PackageRelation::new(
166 /// "example".parse()?,
167 /// Some(VersionRequirement {
168 /// comparison: VersionComparison::Equal,
169 /// version: "1.0.0".parse()?
170 /// })
171 /// ),
172 /// );
173 ///
174 /// assert_eq!(
175 /// PackageRelation::from_str("example>1.0.0")?,
176 /// PackageRelation::new(
177 /// "example".parse()?,
178 /// Some(VersionRequirement {
179 /// comparison: VersionComparison::Greater,
180 /// version: "1.0.0".parse()?
181 /// })
182 /// ),
183 /// );
184 ///
185 /// assert_eq!(
186 /// PackageRelation::from_str("example>=1.0.0")?,
187 /// PackageRelation::new(
188 /// "example".parse()?,
189 /// Some(VersionRequirement {
190 /// comparison: VersionComparison::GreaterOrEqual,
191 /// version: "1.0.0".parse()?
192 /// })
193 /// ),
194 /// );
195 ///
196 /// assert_eq!(
197 /// PackageRelation::from_str("example")?,
198 /// PackageRelation::new("example".parse()?, None),
199 /// );
200 ///
201 /// assert!(PackageRelation::from_str("example<").is_err());
202 /// # Ok(())
203 /// # }
204 /// ```
205 fn from_str(s: &str) -> Result<Self, Self::Err> {
206 Ok(Self::parser.parse(s)?)
207 }
208}
209
210/// An optional dependency for a package.
211///
212/// This type is used for representing dependencies that are not essential for base functionality
213/// of a package, but may be necessary to make use of certain features of a package.
214///
215/// An [`OptionalDependency`] consists of a package relation and an optional description separated
216/// by a colon (`:`).
217///
218/// - The package relation component must be a valid [`PackageRelation`].
219/// - If a description is provided it must be at least one character long.
220///
221/// Refer to [alpm-package-relation] of type [optional dependency] for details on the format.
222/// ## Examples
223///
224/// ```
225/// use std::str::FromStr;
226///
227/// use alpm_types::{Name, OptionalDependency};
228///
229/// # fn main() -> Result<(), alpm_types::Error> {
230/// // Create OptionalDependency from &str
231/// let opt_depend = OptionalDependency::from_str("example: this is an example dependency")?;
232///
233/// // Get the name
234/// assert_eq!("example", opt_depend.name().as_ref());
235///
236/// // Get the description
237/// assert_eq!(
238/// Some("this is an example dependency"),
239/// opt_depend.description().as_deref()
240/// );
241///
242/// // Format as String
243/// assert_eq!(
244/// "example: this is an example dependency",
245/// format!("{opt_depend}")
246/// );
247/// # Ok(())
248/// # }
249/// ```
250///
251/// [alpm-package-relation]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html
252/// [optional dependency]: https://alpm.archlinux.page/specifications/alpm-package-relation.7.html#optional-dependency
253#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
254pub struct OptionalDependency {
255 package_relation: PackageRelation,
256 description: Option<String>,
257}
258
259impl OptionalDependency {
260 /// Create a new OptionalDependency in a Result
261 pub fn new(
262 package_relation: PackageRelation,
263 description: Option<String>,
264 ) -> OptionalDependency {
265 OptionalDependency {
266 package_relation,
267 description,
268 }
269 }
270
271 /// Return the name of the optional dependency
272 pub fn name(&self) -> &Name {
273 &self.package_relation.name
274 }
275
276 /// Return the version requirement of the optional dependency
277 pub fn version_requirement(&self) -> &Option<VersionRequirement> {
278 &self.package_relation.version_requirement
279 }
280
281 /// Return the description for the optional dependency, if it exists
282 pub fn description(&self) -> &Option<String> {
283 &self.description
284 }
285
286 /// Returns a reference to the tracked [`PackageRelation`].
287 pub fn package_relation(&self) -> &PackageRelation {
288 &self.package_relation
289 }
290}
291
292impl AlpmParser for OptionalDependency {
293 /// Recognizes an [`OptionalDependency`] in a string slice.
294 ///
295 /// This format is inherently flawed, as the `:` delimiter may exist in two different, optional
296 /// places.
297 /// 1. **After** the optional epoch
298 /// 2. **Before** the optional description
299 ///
300 /// The `:` delimiter may also appear **inside** the description, although that isn't an issue
301 /// during parsing.
302 ///
303 /// ```text
304 /// why>=1:17.0.1-5: my dependency
305 /// is>=1:17.0.1-5
306 /// it>=17.0.1-5: my other dependency :::::
307 /// this: 1:17.0.1-5 my other dependency
308 /// way>1: 17.0.1-5 ambiguous.
309 /// ```
310 ///
311 /// Due to this, the parser disambiguates the two cases as follows:
312 ///
313 /// - A `:` directly followed by a non-whitespace character is considered an epoch delimiter.
314 /// - A `:` followed by whitespace starts a description.
315 ///
316 /// As such, ambiguous input like `example>=1:foo bar` is treated as containing an epoch and
317 /// rejected, as `foo bar` is not a valid version.
318 /// Input like `example>=1: 3.2.1-5 foo bar` is successfully parsed with the version being `1`
319 /// and the description being `3.2.1-5 foo bar`.
320 ///
321 /// # Errors
322 ///
323 /// Returns an error if `input` is not a valid _alpm-package-relation_ of type _optional
324 /// dependency_.
325 fn parser(input: &mut &str) -> ModalResult<Self> {
326 // Due to the ambiguous nature of this format, we must implement our own PackageRelation and
327 // VersionRequirement parser handling.
328
329 // Handle the dependency name:
330 // `example>=1.0.0: my-description` -> `>=1.0.0: my-description`
331 let name = Name::parser
332 .context(StrContext::Label("package name"))
333 .parse_next(input)?;
334
335 // Handle the optional Comparison operator:
336 // `example>=1.0.0: my-description` -> `1.0.0: my-description`
337 let comparison = opt(VersionComparison::parser).parse_next(input)?;
338
339 // Branch into the path where a comparison exists.
340 let version_requirement = if let Some(comparison) = comparison {
341 // Parse an optional epoch, e.g.:
342 // "1:17.0.1-5: my-description" -> "17.0.1-5: my-description"
343 //
344 // An epoch delimiter ':' must always be directly followed by a non-whitespace
345 // character, while a description ':' delimiter is always followed by whitespace.
346 // The lookahead on the character after the ':' disambiguates the two.
347 let epoch = opt(terminated(
348 Epoch::parser_until_inclusive(":"),
349 peek(none_of(|c: char| c.is_whitespace())),
350 ))
351 .parse_next(input)?;
352
353 // Advance the parser until the next '-', e.g.:
354 // "17.0.1-5: my-description" -> "-5: my-description"
355 let pkgver = PackageVersion::parser.parse_next(input)?;
356
357 // Parse an optional PackageRelease, e.g.:
358 // "-5: my-description" -> ": my-description"
359 //
360 // If an `-` is found, the PackageRelease is expected and must exist
361 let delimiter = opt('-').parse_next(input)?;
362 let pkgrel = if delimiter.is_some() {
363 Some(PackageRelease::parser.parse_next(input)?)
364 } else {
365 None
366 };
367
368 Some(VersionRequirement {
369 comparison,
370 version: Version::new(pkgver, epoch, pkgrel),
371 })
372 } else {
373 None
374 };
375
376 let package_relation = PackageRelation::new(name, version_requirement);
377
378 // Check if there's a `:`, which indicates the existence of an description.
379 let delimiter = opt(":").parse_next(input)?;
380 if delimiter.is_some() {
381 space1
382 .context(StrContext::Label(
383 "dependency delimiter in optional dependency",
384 ))
385 .context(StrContext::Expected(StrContextValue::Description(
386 "A colon followed by a whitespace ': '",
387 )))
388 .parse_next(input)?;
389 }
390
391 let description = if delimiter.is_some() {
392 // Descriptions are at the end of a `OptionalDependency` and may contain any character,
393 // except '\n' or '\r'. So this parser consumes everything till newline or `eof`.
394 let description = take_till(0.., ('\n', '\r'))
395 .context(StrContext::Label("optional dependency description"))
396 .parse_next(input)?
397 .trim_ascii();
398
399 if description.is_empty() {
400 None
401 } else {
402 Some(description.to_string())
403 }
404 } else {
405 None
406 };
407
408 Ok(Self {
409 package_relation,
410 description,
411 })
412 }
413
414 fn delimiter_error_context<'a, O, P>(
415 parser: P,
416 ) -> impl Parser<&'a str, O, winnow::error::ErrMode<winnow::error::ContextError>>
417 where
418 P: Parser<&'a str, O, winnow::error::ErrMode<winnow::error::ContextError>>,
419 {
420 parser
421 .context(StrContext::Label("character in optional dependency"))
422 .context(StrContext::Expected(StrContextValue::Description(
423 "end of input.",
424 )))
425 }
426}
427
428impl FromStr for OptionalDependency {
429 type Err = Error;
430
431 /// Creates a new [`OptionalDependency`] from a string slice.
432 ///
433 /// Delegates to [`OptionalDependency::parser`].
434 ///
435 /// # Errors
436 ///
437 /// Returns an error if [`OptionalDependency::parser`] fails.
438 fn from_str(s: &str) -> Result<Self, Self::Err> {
439 Ok(Self::parser_until_eof.parse(s)?)
440 }
441}
442
443impl Display for OptionalDependency {
444 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
445 match self.description {
446 Some(ref description) => write!(fmt, "{}: {}", self.package_relation, description),
447 None => write!(fmt, "{}", self.package_relation),
448 }
449 }
450}
451
452/// Group of a package
453///
454/// Represents an arbitrary collection of packages that share a common
455/// characteristic or functionality.
456///
457/// While group names can be any valid UTF-8 string, it is recommended to follow
458/// the format of [`Name`] (`[a-z\d\-._@+]` but must not start with `[-.]`)
459/// to ensure consistency and ease of use.
460///
461/// This is a type alias for [`String`].
462///
463/// ## Examples
464/// ```
465/// use alpm_types::Group;
466///
467/// // Create a Group
468/// let group: Group = "package-group".to_string();
469/// ```
470pub type Group = String;
471
472#[cfg(test)]
473mod tests {
474 use insta::assert_snapshot;
475 use proptest::{prop_assert_eq, proptest, test_runner::Config as ProptestConfig};
476 use rstest::rstest;
477
478 use super::*;
479 use crate::{VersionComparison, configure_insta};
480
481 const COMPARATOR_REGEX: &str = r"(<|<=|=|>=|>)";
482 /// NOTE: [`Epoch`][alpm_types::Epoch] is implicitly constrained by [`std::usize::MAX`].
483 /// However, it's unrealistic to ever reach that many forced downgrades for a package, hence
484 /// we don't test that fully
485 const EPOCH_REGEX: &str = r"(0|[1-9][0-9]{0,10})";
486 const NAME_REGEX: &str = r"[a-z0-9_@+]+[a-z0-9\-._@+]*";
487 const PKGREL_REGEX: &str = r"[1-9][0-9]{0,8}(|[.][1-9][0-9]{0,8})";
488 const PKGVER_REGEX: &str = r"([[:alnum:]][[:alnum:]_+.]*)";
489 const DESCRIPTION_REGEX: &str = "[^\n\r]*";
490
491 proptest! {
492 #![proptest_config(ProptestConfig::with_cases(1000))]
493
494
495 #[test]
496 fn valid_package_relation_from_str(s in format!("{NAME_REGEX}(|{COMPARATOR_REGEX}(|{EPOCH_REGEX}:){PKGVER_REGEX}(|-{PKGREL_REGEX}))").as_str()) {
497 println!("s: {s}");
498 let name = PackageRelation::from_str(&s).unwrap();
499 prop_assert_eq!(s, format!("{}", name));
500 }
501 }
502
503 proptest! {
504 #[test]
505 fn opt_depend_from_str(
506 name in NAME_REGEX,
507 desc in DESCRIPTION_REGEX,
508 use_desc in proptest::bool::ANY
509 ) {
510 let desc_trimmed = desc.trim_ascii();
511 let desc_is_blank = desc_trimmed.is_empty();
512
513 let (raw_in, formatted_expected) = if use_desc {
514 // Raw input and expected formatted output.
515 // These are different because `desc` will be trimmed by the parser;
516 // if it is *only* ascii whitespace then it will be skipped altogether.
517 (
518 format!("{name}: {desc}"),
519 if !desc_is_blank {
520 format!("{name}: {desc_trimmed}")
521 } else {
522 name.clone()
523 }
524 )
525 } else {
526 (name.clone(), name.clone())
527 };
528
529 println!("input string: {raw_in}");
530 let opt_depend = OptionalDependency::from_str(&raw_in).unwrap();
531 let formatted_actual = format!("{opt_depend}");
532 prop_assert_eq!(
533 formatted_expected,
534 formatted_actual,
535 "Formatted output doesn't match input"
536 );
537 }
538 }
539
540 #[rstest]
541 #[case(
542 "python>=3",
543 Ok(PackageRelation {
544 name: Name::new("python").unwrap(),
545 version_requirement: Some(VersionRequirement {
546 comparison: VersionComparison::GreaterOrEqual,
547 version: "3".parse().unwrap(),
548 }),
549 }),
550 )]
551 #[case(
552 "java-environment>=17",
553 Ok(PackageRelation {
554 name: Name::new("java-environment").unwrap(),
555 version_requirement: Some(VersionRequirement {
556 comparison: VersionComparison::GreaterOrEqual,
557 version: "17".parse().unwrap(),
558 }),
559 }),
560 )]
561 fn valid_package_relation(
562 #[case] input: &str,
563 #[case] expected: Result<PackageRelation, Error>,
564 ) {
565 assert_eq!(PackageRelation::from_str(input), expected);
566 }
567
568 #[rstest]
569 #[case(
570 "example: this is an example dependency",
571 OptionalDependency {
572 package_relation: PackageRelation {
573 name: Name::new("example").unwrap(),
574 version_requirement: None,
575 },
576 description: Some("this is an example dependency".to_string()),
577 },
578 )]
579 #[case(
580 "example-two: a description with lots of whitespace padding ",
581 OptionalDependency {
582 package_relation: PackageRelation {
583 name: Name::new("example-two").unwrap(),
584 version_requirement: None,
585 },
586 description: Some("a description with lots of whitespace padding".to_string())
587 },
588 )]
589 #[case(
590 "dep_name",
591 OptionalDependency {
592 package_relation: PackageRelation {
593 name: Name::new("dep_name").unwrap(),
594 version_requirement: None,
595 },
596 description: None,
597 },
598 )]
599 #[case(
600 "dep_name: ",
601 OptionalDependency {
602 package_relation: PackageRelation {
603 name: Name::new("dep_name").unwrap(),
604 version_requirement: None,
605 },
606 description: None,
607 },
608 )]
609 #[case(
610 "dep_name_with_special_chars-123: description with !@#$%^&*",
611 OptionalDependency {
612 package_relation: PackageRelation {
613 name: Name::new("dep_name_with_special_chars-123").unwrap(),
614 version_requirement: None,
615 },
616 description: Some("description with !@#$%^&*".to_string()),
617 },
618 )]
619 // versioned optional dependencies
620 #[case(
621 "elfutils=0.192: for translations",
622 OptionalDependency {
623 package_relation: PackageRelation {
624 name: Name::new("elfutils").unwrap(),
625 version_requirement: Some(VersionRequirement {
626 comparison: VersionComparison::Equal,
627 version: "0.192".parse().unwrap(),
628 }),
629 },
630 description: Some("for translations".to_string()),
631 },
632 )]
633 #[case(
634 "python>=3: For Python bindings",
635 OptionalDependency {
636 package_relation: PackageRelation {
637 name: Name::new("python").unwrap(),
638 version_requirement: Some(VersionRequirement {
639 comparison: VersionComparison::GreaterOrEqual,
640 version: "3".parse().unwrap(),
641 }),
642 },
643 description: Some("For Python bindings".to_string()),
644 },
645 )]
646 #[case(
647 "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver",
648 OptionalDependency {
649 package_relation: PackageRelation {
650 name: Name::new("java-environment").unwrap(),
651 version_requirement: Some(VersionRequirement {
652 comparison: VersionComparison::GreaterOrEqual,
653 version: "17".parse().unwrap(),
654 }),
655 },
656 description: Some("required by extension-wiki-publisher and extension-nlpsolver".to_string()),
657 },
658 )]
659 // A ':' directly followed by a non-whitespace character acts as an epoch delimiter.
660 #[case(
661 "example>=1:17.0.1-5: my dependency",
662 OptionalDependency {
663 package_relation: PackageRelation {
664 name: Name::new("example").unwrap(),
665 version_requirement: Some(VersionRequirement {
666 comparison: VersionComparison::GreaterOrEqual,
667 version: "1:17.0.1-5".parse().unwrap(),
668 }),
669 },
670 description: Some("my dependency".to_string()),
671 },
672 )]
673 // A ':' followed by whitespace acts as a description delimiter.
674 #[case(
675 "example>1: 17.0.1-5 ambiguous.",
676 OptionalDependency {
677 package_relation: PackageRelation {
678 name: Name::new("example").unwrap(),
679 version_requirement: Some(VersionRequirement {
680 comparison: VersionComparison::Greater,
681 version: "1".parse().unwrap(),
682 }),
683 },
684 description: Some("17.0.1-5 ambiguous.".to_string()),
685 },
686 )]
687 fn opt_depend_from_string(#[case] input: &str, #[case] expected: OptionalDependency) {
688 let opt_depend_result = OptionalDependency::from_str(input);
689 let optional_dependency = match opt_depend_result {
690 Ok(dep) => dep,
691 Err(err) => {
692 panic!("Encountered unexpected error when parsing optional dependency:\n {err}")
693 }
694 };
695
696 assert_eq!(
697 expected, optional_dependency,
698 "Optional dependency has not been correctly parsed."
699 );
700 }
701
702 #[rstest]
703 #[case(
704 "example: this is an example dependency",
705 "example: this is an example dependency"
706 )]
707 #[case(
708 "example-two: a description with lots of whitespace padding ",
709 "example-two: a description with lots of whitespace padding"
710 )]
711 #[case(
712 "tabs: a description with a tab directly after the colon",
713 "tabs: a description with a tab directly after the colon"
714 )]
715 #[case("dep_name", "dep_name")]
716 #[case("dep_name: ", "dep_name")]
717 #[case(
718 "dep_name_with_special_chars-123: description with !@#$%^&*",
719 "dep_name_with_special_chars-123: description with !@#$%^&*"
720 )]
721 // versioned optional dependencies
722 #[case("elfutils=0.192: for translations", "elfutils=0.192: for translations")]
723 #[case("python>=3: For Python bindings", "python>=3: For Python bindings")]
724 #[case(
725 "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver",
726 "java-environment>=17: required by extension-wiki-publisher and extension-nlpsolver"
727 )]
728 fn opt_depend_to_string(#[case] input: &str, #[case] expected: &str) {
729 let opt_depend_result = OptionalDependency::from_str(input);
730 let Ok(optional_dependency) = opt_depend_result else {
731 panic!(
732 "Encountered unexpected error when parsing optional dependency: {opt_depend_result:?}"
733 )
734 };
735 assert_eq!(
736 expected,
737 optional_dependency.to_string(),
738 "OptionalDependency to_string is erroneous."
739 );
740 }
741
742 #[rstest]
743 #[case("#invalid-name: this is an example dependency")]
744 #[case(": no_name_colon")]
745 #[case("name:description with no leading whitespace")]
746 #[case("dep-name>=10: \n\ndescription with\rnewlines")]
747 fn opt_depend_invalid_string_parse_error(#[case] input: &str) {
748 let Err(Error::ParseError(err_msg)) = OptionalDependency::from_str(input) else {
749 panic!("'{input}' erroneously parsed as a OptionalDependency")
750 };
751
752 let (test_name, _guard) = configure_insta();
753 assert_snapshot!(test_name, err_msg.to_string());
754 }
755}