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