Skip to main content

nix_compat/derivation/
outputs.rs

1//! Outputs for a derivation.
2use std::{collections::BTreeMap, fmt::Write as _};
3
4use crate::{
5    derivation::{Output, OutputHash, OutputHashMode, OutputName, output::ParseOutputError},
6    store_path,
7};
8
9/// Errors that can occur during the creation and validation of [`Outputs`].
10#[derive(Debug, PartialEq, thiserror::Error)]
11pub enum OutputsError {
12    #[error("no outputs defined")]
13    NoOutputs(),
14    #[error("invalid output name: {0}")]
15    InvalidOutputName(String),
16    #[error("duplicate output name: {0}")]
17    DuplicateOutputName(OutputName),
18    #[error("encountered fixed-output derivation, but more than 1 output in total")]
19    MoreThanOneOutputButFixed(),
20    #[error("invalid output name for fixed-output derivation: {0}")]
21    InvalidOutputNameForFixed(String),
22    #[error("unable to validate output {0}: {1}")]
23    InvalidOutput(String, #[source] ParseOutputError),
24    #[error("invalid calculated output derivation path name: {0}")]
25    InvalidOutputDerivationPath(String, #[source] store_path::ParseStorePathError),
26}
27
28/// An iterator over the entries of `Outputs`.
29///
30/// This `struct` is created by the [`iter`] method on [`Outputs`]. See its
31/// documentation for more.
32///
33/// [`iter`]: Outputs::iter
34pub struct Iter<'a>(IterI<'a>);
35
36enum IterI<'a> {
37    Single(std::iter::Once<(&'a OutputName, &'a Output)>),
38    Multiple(std::collections::btree_map::Iter<'a, OutputName, Output>),
39}
40
41impl<'a> Iterator for Iter<'a> {
42    type Item = (&'a OutputName, &'a Output);
43
44    fn next(&mut self) -> Option<Self::Item> {
45        match &mut self.0 {
46            IterI::Single(it) => it.next(),
47            IterI::Multiple(it) => it.next(),
48        }
49    }
50    fn size_hint(&self) -> (usize, Option<usize>) {
51        match &self.0 {
52            IterI::Single(it) => it.size_hint(),
53            IterI::Multiple(it) => it.size_hint(),
54        }
55    }
56}
57
58impl<'a> ExactSizeIterator for Iter<'a> {}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
61enum OutputsInner {
62    Single(Output),
63    Multiple(BTreeMap<OutputName, Output>),
64}
65
66/// Derivation outputs.
67///
68/// Outputs are generally mappings from [`OutputName`] to a [`Output`] but
69/// with some extra invariants.
70///
71/// # Invariants
72/// - If there is only one output it is named `out`
73/// - Only a single FOD output is allowed
74/// - Duplicately named outputs is an error
75/// - Outputs can not be empty
76///
77/// # `StorePath` for an `Output`
78/// When a derivation is being constructed the [`StorePath`] of each output
79/// is not known yet and so `Outputs` does support this.
80///
81/// But generally you should call [`calculate_output_paths`] and after that
82/// the [`StorePath`] of each output is available and will never change.
83///
84/// It is for this reason that the only method mutating `Outputs` after
85/// construction is [`calculate_output_paths`].
86///
87/// [`StorePath`]: store_path::StorePath
88/// [`calculate_output_paths`]: Outputs::calculate_output_paths
89#[derive(Clone, Debug, PartialEq, Eq)]
90pub struct Outputs(OutputsInner);
91
92impl Outputs {
93    /// Return `Ouputs` with single fixed-output that matches `output_hash`.
94    ///
95    /// # Examples
96    /// ```
97    /// use nix_compat::derivation::{Outputs, OutputName};
98    /// # use nix_compat::derivation::{OutputHash, OutputHashMode};
99    /// # use nix_compat::nixhash::NixHash;
100    ///
101    /// # const DIGEST_SHA256: [u8; 32] =
102    /// #     hex_literal::hex!("a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39");
103    /// # const NIXHASH_SHA256: NixHash = NixHash::Sha256(DIGEST_SHA256);
104    /// # let output_hash = OutputHash { mode: OutputHashMode::Flat, hash: NIXHASH_SHA256.clone() };
105    /// let b = Outputs::from_fod_hash(output_hash);
106    /// assert!(b.is_single());
107    /// assert!(b.is_fixed());
108    /// assert!(b.contains_key(&OutputName::out()));
109    /// ```
110    pub const fn from_fod_hash(output_hash: OutputHash) -> Self {
111        Outputs(OutputsInner::Single(Output {
112            path: None,
113            output_hash: Some(output_hash),
114        }))
115    }
116
117    /// Return a new `Ouputs` with just a single output.
118    ///
119    /// That single output is always called `out`.
120    ///
121    /// # Examples
122    /// ```
123    /// use nix_compat::derivation::{Outputs, OutputName};
124    ///
125    /// let b = Outputs::with_single_output();
126    /// assert!(b.is_single());
127    /// assert!(b.contains_key(&OutputName::out()));
128    /// ```
129    pub const fn with_single_output() -> Self {
130        Outputs(OutputsInner::Single(Output {
131            path: None,
132            output_hash: None,
133        }))
134    }
135
136    /// Try to make `Outputs` from the provided iterator.
137    ///
138    /// This will return a [`OutputsError`] if the [`OutputName`], [`Output`] pairs
139    /// in the iterator don't follow the [invariants].
140    ///
141    /// [invariants]: #invariants
142    pub fn try_from_iter<I>(it: I) -> Result<Self, OutputsError>
143    where
144        I: IntoIterator<Item = (OutputName, Output)>,
145    {
146        let mut it = it.into_iter();
147        let Some((output_name, output)) = it.next() else {
148            return Err(OutputsError::NoOutputs());
149        };
150        if let Some((second_name, second_output)) = it.next() {
151            if output.is_fixed() || second_output.is_fixed() {
152                return Err(OutputsError::MoreThanOneOutputButFixed());
153            }
154            let mut outputs = BTreeMap::new();
155            outputs.insert(output_name, output);
156            if outputs.insert(second_name.clone(), second_output).is_some() {
157                return Err(OutputsError::DuplicateOutputName(second_name));
158            }
159            for (output_name, output) in it {
160                if output.is_fixed() {
161                    return Err(OutputsError::MoreThanOneOutputButFixed());
162                }
163                if outputs.insert(output_name.clone(), output).is_some() {
164                    return Err(OutputsError::DuplicateOutputName(output_name));
165                }
166            }
167            Ok(Outputs(OutputsInner::Multiple(outputs)))
168        } else if output_name == OutputName::out() {
169            Ok(Outputs(OutputsInner::Single(output)))
170        } else {
171            Err(OutputsError::InvalidOutputName(output_name.to_string()))
172        }
173    }
174
175    /// Returns `true` if the outputs contains an output with the specified `name`.
176    #[must_use]
177    pub fn contains_key(&self, name: &OutputName) -> bool {
178        match &self.0 {
179            OutputsInner::Single(_) => *name == OutputName::out(),
180            OutputsInner::Multiple(outputs) => outputs.contains_key(name),
181        }
182    }
183
184    /// Returns a reference to the [`Output`] corresponding to the provided `name`.
185    pub fn get(&self, name: &OutputName) -> Option<&Output> {
186        match &self.0 {
187            OutputsInner::Single(output) if *name == OutputName::out() => Some(output),
188            OutputsInner::Multiple(outputs) => outputs.get(name),
189            _ => None,
190        }
191    }
192
193    /// Gets an iterator over the entries of the outputs, sorted by name.
194    pub fn iter(&self) -> Iter<'_> {
195        match &self.0 {
196            OutputsInner::Single(output) => {
197                const OUT: &OutputName = &OutputName::out();
198                Iter(IterI::Single(std::iter::once((OUT, output))))
199            }
200            OutputsInner::Multiple(outputs) => Iter(IterI::Multiple(outputs.iter())),
201        }
202    }
203
204    /// Gets an iterator over the names of the outputs, in sorted order.
205    ///
206    /// # Examples
207    /// ```
208    /// use nix_compat::derivation::{Outputs, OutputName};
209    ///
210    /// let a = Outputs::try_from_iter([
211    ///     (OutputName::out(), Default::default()),
212    ///     (OutputName::from_static("bin").unwrap(), Default::default()),
213    /// ]).expect("multiple outputs");
214    ///
215    /// let keys: Vec<OutputName> = a.keys().cloned().collect();
216    /// assert_eq!(keys, [
217    ///     OutputName::from_static("bin").unwrap(),
218    ///     OutputName::out(),
219    /// ]);
220    /// ```
221    pub fn keys(&self) -> impl Iterator<Item = &OutputName> {
222        self.iter().map(|(name, _)| name)
223    }
224
225    /// Gets an iterator over the [`Output`] values of the outputs, in order by name.
226    ///
227    /// # Examples
228    /// ```
229    /// use nix_compat::derivation::{Output, Outputs, OutputName};
230    ///
231    /// let a = Outputs::with_single_output();
232    ///
233    /// let values: Vec<Output> = a.values().cloned().collect();
234    /// assert_eq!(values, [Output::default()]);
235    /// ```
236    pub fn values(&self) -> impl Iterator<Item = &Output> {
237        self.iter().map(|(_, output)| output)
238    }
239
240    /// Returns the number of outputs
241    ///
242    /// # Examples
243    /// ```
244    /// use nix_compat::derivation::{Outputs, OutputName};
245    ///
246    /// let a = Outputs::with_single_output();
247    /// assert_eq!(a.len(), 1);
248    ///
249    /// let b = Outputs::try_from_iter([
250    ///     (OutputName::out(), Default::default()),
251    ///     (OutputName::from_static("bin").unwrap(), Default::default()),
252    /// ]).expect("multiple outputs");
253    /// assert_eq!(b.len(), 2);
254    /// ```
255    #[expect(clippy::len_without_is_empty)]
256    pub fn len(&self) -> usize {
257        match &self.0 {
258            OutputsInner::Single(_) => 1,
259            OutputsInner::Multiple(outputs) => outputs.len(),
260        }
261    }
262
263    /// Returns `true` if this contains only a single output.
264    ///
265    /// # Examples
266    ///
267    /// ```
268    /// use nix_compat::derivation::{Outputs, OutputName};
269    /// # use nix_compat::derivation::{OutputHash, OutputHashMode};
270    /// # use nix_compat::nixhash::NixHash;
271    ///
272    /// let a = Outputs::with_single_output();
273    /// assert!(a.is_single());
274    ///
275    /// # const DIGEST_SHA256: [u8; 32] =
276    /// #     hex_literal::hex!("a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39");
277    /// # const NIXHASH_SHA256: NixHash = NixHash::Sha256(DIGEST_SHA256);
278    /// # let output_hash = OutputHash { mode: OutputHashMode::Flat, hash: NIXHASH_SHA256.clone() };
279    /// let b = Outputs::from_fod_hash(output_hash);
280    /// assert!(b.is_single());
281    ///
282    /// let c = Outputs::try_from_iter([
283    ///     (OutputName::out(), Default::default()),
284    ///     (OutputName::from_static("bin").unwrap(), Default::default()),
285    /// ]).unwrap();
286    /// assert!(!c.is_single());
287    /// ```
288    #[must_use]
289    pub fn is_single(&self) -> bool {
290        self.len() == 1
291    }
292
293    /// Returns `true` if this is a single fixed-output.
294    #[must_use]
295    pub fn is_fixed(&self) -> bool {
296        matches!(&self.0, OutputsInner::Single(out) if out.is_fixed())
297    }
298
299    /// This calculates all output paths of a `Outputs` and updates the struct.
300    ///
301    /// It requires the struct to be initially without output paths.
302    /// This means, [`Output::path`] needs to be `None` for each output.
303    ///
304    /// Output path calculation requires knowledge of the
305    /// [`hash_derivation_modulo`], which (in case of non-fixed-output
306    /// derivations) also requires knowledge of the
307    /// [`hash_derivation_modulo`] of input derivations (recursively).
308    ///
309    /// To avoid recursing and doing unnecessary calculation, we simply
310    /// ask the caller of this function to provide the result of the
311    /// [`hash_derivation_modulo`] call of the current [`Derivation`],
312    /// and leave it up to them to calculate it when needed.
313    ///
314    /// On completion, [`Output::path`] of each output is set to the calculated output path.
315    ///
316    /// [`hash_derivation_modulo`]: super::Derivation::hash_derivation_modulo
317    /// [`Derivation`]: super::Derivation
318    pub fn calculate_output_paths(
319        &mut self,
320        drv_name: &str,
321        hash_derivation_modulo: &[u8; 32],
322    ) -> Result<(), OutputsError> {
323        match &mut self.0 {
324            OutputsInner::Single(output) => {
325                // Assert that outputs are not yet populated, to avoid using this function wrongly.
326                // We don't also go over self.environment, but it's a sufficient
327                // footgun prevention mechanism.
328                assert!(output.path.is_none());
329
330                // Assemble the name, which is either the drv-name suffixed `-{output_name}`,
331                // except in the `out` case, where it's omitted.
332                let name = drv_name.to_owned();
333
334                // For fixed output derivation we use [build_ca_path], otherwise we
335                // use [build_output_path] with [hash_derivation_modulo].
336                let store_path = if let Some(output_hash) = &output.output_hash {
337                    store_path::build_ca_path(
338                        &name,
339                        output_hash.mode == OutputHashMode::Recursive,
340                        &output_hash.hash,
341                        [],
342                        false,
343                    )
344                } else {
345                    store_path::build_output_path(&name, hash_derivation_modulo, &OutputName::out())
346                }
347                .map_err(|e| OutputsError::InvalidOutputDerivationPath(name.to_string(), e))?;
348
349                output.path = Some(store_path.to_owned());
350            }
351            OutputsInner::Multiple(outputs) => {
352                for (output_name, output) in outputs.iter_mut() {
353                    assert!(!output.is_fixed(), "Snix bug: multiple FOD");
354
355                    // Assemble the name, which is either the drv-name suffixed `-{output_name}`,
356                    // except in the `out` case, where it's omitted.
357                    let name = {
358                        let mut name = drv_name.to_owned();
359                        if output_name != &OutputName::out() {
360                            name.write_fmt(format_args!("-{output_name}")).unwrap();
361                        }
362                        name
363                    };
364
365                    // use [build_output_path] with [hash_derivation_modulo].
366                    let store_path =
367                        store_path::build_output_path(&name, hash_derivation_modulo, output_name)
368                            .map_err(|e| {
369                            OutputsError::InvalidOutputDerivationPath(name.to_string(), e)
370                        })?;
371
372                    output.path = Some(store_path.to_owned());
373                }
374            }
375        }
376        Ok(())
377    }
378}
379
380impl<'a> IntoIterator for &'a Outputs {
381    type Item = (&'a OutputName, &'a Output);
382
383    type IntoIter = Iter<'a>;
384
385    fn into_iter(self) -> Self::IntoIter {
386        self.iter()
387    }
388}
389
390impl Default for Outputs {
391    fn default() -> Self {
392        Self(OutputsInner::Single(Default::default()))
393    }
394}
395
396impl TryFrom<BTreeMap<OutputName, Output>> for Outputs {
397    type Error = OutputsError;
398
399    fn try_from(outputs: BTreeMap<OutputName, Output>) -> Result<Self, Self::Error> {
400        Self::try_from_iter(outputs)
401    }
402}
403
404#[cfg(test)]
405impl Outputs {
406    /// Clear any [`StorePath`] from each output.
407    pub fn trim_store_paths(&mut self) {
408        match &mut self.0 {
409            OutputsInner::Single(output) => {
410                output.path = None;
411            }
412            OutputsInner::Multiple(outputs) => {
413                for output in outputs.values_mut() {
414                    output.path = None;
415                }
416            }
417        }
418    }
419}
420
421#[cfg(feature = "serde")]
422impl<'de> serde::Deserialize<'de> for Outputs {
423    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
424    where
425        D: serde::Deserializer<'de>,
426    {
427        struct OutputsVisitor;
428        impl<'de> serde::de::Visitor<'de> for OutputsVisitor {
429            type Value = Outputs;
430
431            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
432                f.write_str("derivation outputs")
433            }
434
435            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
436            where
437                A: serde::de::MapAccess<'de>,
438            {
439                use serde::de::{Error, Unexpected};
440                let Some((output_name, output)) = map.next_entry::<OutputName, Output>()? else {
441                    return Err(A::Error::invalid_length(0, &"non-empty derivation outputs"));
442                };
443                if let Some((second_name, second_output)) =
444                    map.next_entry::<OutputName, Output>()?
445                {
446                    if output.is_fixed() || second_output.is_fixed() {
447                        return Err(A::Error::invalid_value(
448                            Unexpected::Other("FOD output"),
449                            &"non-FOD outputs",
450                        ));
451                    }
452                    let mut outputs = BTreeMap::new();
453                    outputs.insert(output_name, output);
454                    if outputs.insert(second_name, second_output).is_some() {
455                        return Err(A::Error::custom(format_args!(
456                            "duplicate derivation output"
457                        )));
458                    }
459                    while let Some((output_name, output)) =
460                        map.next_entry::<OutputName, Output>()?
461                    {
462                        if output.is_fixed() {
463                            return Err(A::Error::invalid_value(
464                                Unexpected::Other("FOD output"),
465                                &"non-FOD outputs",
466                            ));
467                        }
468                        if outputs.insert(output_name, output).is_some() {
469                            return Err(A::Error::custom(format_args!(
470                                "duplicate derivation output"
471                            )));
472                        }
473                    }
474                    Ok(Outputs(OutputsInner::Multiple(outputs)))
475                } else if output_name == OutputName::out() {
476                    Ok(Outputs(OutputsInner::Single(output)))
477                } else {
478                    Err(A::Error::invalid_value(
479                        Unexpected::Other(output_name.as_str()),
480                        &"single derivation output named 'out'",
481                    ))
482                }
483            }
484        }
485
486        deserializer.deserialize_map(OutputsVisitor)
487    }
488}
489
490#[cfg(feature = "serde")]
491impl serde::Serialize for Outputs {
492    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
493    where
494        S: serde::Serializer,
495    {
496        use serde::ser::SerializeMap as _;
497        let mut map = serializer.serialize_map(Some(self.len()))?;
498        for (k, v) in self {
499            map.serialize_entry(k, v)?;
500        }
501        map.end()
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use std::{collections::BTreeMap, sync::LazyLock};
508
509    use rstest::rstest;
510
511    use crate::{
512        derivation::{Output, OutputHash, OutputHashMode, OutputName, Outputs},
513        nixhash::NixHash,
514    };
515
516    const DIGEST_SHA256: [u8; 32] =
517        hex_literal::hex!("a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39");
518    const OUTPUT_HASH: OutputHash = OutputHash {
519        mode: OutputHashMode::Flat,
520        hash: NixHash::Sha256(DIGEST_SHA256),
521    };
522    const FOD_OUTPUT: Output = Output {
523        output_hash: Some(OutputHash {
524            mode: OutputHashMode::Flat,
525            hash: NixHash::Sha256(DIGEST_SHA256),
526        }),
527        path: None,
528    };
529
530    const SINGLE_OUTPUTS: Outputs = Outputs::with_single_output();
531    const FOD_OUTPUTS: Outputs = Outputs::from_fod_hash(OutputHash {
532        mode: OutputHashMode::Flat,
533        hash: NixHash::Sha256(DIGEST_SHA256),
534    });
535    static TRY_SINGLE: LazyLock<Outputs> = LazyLock::new(|| {
536        Outputs::try_from_iter([(OutputName::out(), Default::default())]).expect("single output")
537    });
538    static TRY_FOD: LazyLock<Outputs> = LazyLock::new(|| -> Outputs {
539        Outputs::try_from_iter([(
540            OutputName::out(),
541            Output {
542                path: None,
543                output_hash: Some(OUTPUT_HASH.clone()),
544            },
545        )])
546        .expect("single fod")
547    });
548    static TRY_MULTIPLE: LazyLock<Outputs> = LazyLock::new(|| -> Outputs {
549        Outputs::try_from_iter([
550            (OutputName::from_static("dev").unwrap(), Default::default()),
551            (OutputName::from_static("bin").unwrap(), Default::default()),
552        ])
553        .expect("multiple")
554    });
555
556    #[rstest]
557    #[should_panic(expected = "no outputs defined")]
558    #[case::empty(&[])]
559    #[should_panic(expected = "invalid output name: bin")]
560    #[case::single(&[(OutputName::from_static("bin").unwrap(), Default::default())])]
561    #[should_panic(expected = "duplicate output name: bin")]
562    #[case::duplicate(&[
563        (OutputName::from_static("bin").unwrap(), Default::default()),
564        (OutputName::from_static("bin").unwrap(), Default::default()),
565    ])]
566    #[should_panic(
567        expected = "encountered fixed-output derivation, but more than 1 output in total"
568    )]
569    #[case::mixed(&[
570        (OutputName::from_static("bin").unwrap(), FOD_OUTPUT.clone()),
571        (OutputName::from_static("dev").unwrap(), Default::default()),
572    ])]
573    fn try_from_iter_failure(#[case] it: &[(OutputName, Output)]) {
574        panic!(
575            "{}",
576            Outputs::try_from_iter(it.iter().cloned()).expect_err("try_from_iter succeeded")
577        );
578    }
579
580    #[rstest]
581    #[should_panic(expected = "no outputs defined")]
582    #[case::empty(&[])]
583    #[should_panic(expected = "invalid output name: bin")]
584    #[case::single(&[(OutputName::from_static("bin").unwrap(), Default::default())])]
585    #[should_panic(
586        expected = "encountered fixed-output derivation, but more than 1 output in total"
587    )]
588    #[case::mixed(&[
589        (OutputName::from_static("bin").unwrap(), FOD_OUTPUT.clone()),
590        (OutputName::from_static("dev").unwrap(), Default::default()),
591    ])]
592    fn try_from_failure(#[case] it: &[(OutputName, Output)]) {
593        let b = BTreeMap::from_iter(it.iter().cloned());
594        panic!("{}", Outputs::try_from(b).expect_err("try_from succeeded"));
595    }
596
597    #[rstest]
598    #[case::single(&SINGLE_OUTPUTS)]
599    #[case::fod(&FOD_OUTPUTS)]
600    #[case::try_single(&TRY_SINGLE)]
601    #[case::try_fod(&TRY_FOD)]
602    #[case::default(&Default::default())]
603    fn is_single(#[case] value: &Outputs) {
604        assert!(value.is_single())
605    }
606
607    #[rstest]
608    #[case::multiple(&TRY_MULTIPLE)]
609    fn is_not_single(#[case] value: &Outputs) {
610        assert!(!value.is_single())
611    }
612
613    #[rstest]
614    #[case::fod(&FOD_OUTPUTS)]
615    #[case::try_fod(&TRY_FOD)]
616    fn is_fixed(#[case] value: &Outputs) {
617        assert!(value.is_fixed())
618    }
619
620    #[rstest]
621    #[case::single(&SINGLE_OUTPUTS)]
622    #[case::try_single(&TRY_SINGLE)]
623    #[case::multiple(&TRY_MULTIPLE)]
624    #[case::default(&Default::default())]
625    fn is_not_fixed(#[case] value: &Outputs) {
626        assert!(!value.is_fixed())
627    }
628
629    #[rstest]
630    #[case::single(&SINGLE_OUTPUTS, 1)]
631    #[case::fod(&FOD_OUTPUTS, 1)]
632    #[case::try_fod(&TRY_FOD, 1)]
633    #[case::try_single(&TRY_SINGLE, 1)]
634    #[case::multiple(&TRY_MULTIPLE, 2)]
635    #[case::default(&Default::default(), 1)]
636    fn len(#[case] value: &Outputs, #[case] expected: usize) {
637        assert_eq!(value.len(), expected)
638    }
639
640    #[rstest]
641    #[case::single(&SINGLE_OUTPUTS, OutputName::out(), Some(&Default::default()))]
642    #[case::fod(&FOD_OUTPUTS, OutputName::out(), Some(&FOD_OUTPUT))]
643    #[case::try_fod(&TRY_FOD, OutputName::out(), Some(&FOD_OUTPUT))]
644    #[case::try_single(&TRY_SINGLE, OutputName::out(), Some(&Default::default()))]
645    #[case::multiple_out(&TRY_MULTIPLE, OutputName::out(), None)]
646    #[case::multiple_bin(&TRY_MULTIPLE, OutputName::from_static("bin").unwrap(), Some(&Default::default()))]
647    #[case::default(&Default::default(), OutputName::out(), Some(&Default::default()))]
648    fn get(#[case] value: &Outputs, #[case] name: OutputName, #[case] expected: Option<&Output>) {
649        assert_eq!(value.get(&name), expected)
650    }
651
652    #[rstest]
653    #[case::single(&SINGLE_OUTPUTS, OutputName::out())]
654    #[case::fod(&FOD_OUTPUTS, OutputName::out())]
655    #[case::try_fod(&TRY_FOD, OutputName::out())]
656    #[case::try_single(&TRY_SINGLE, OutputName::out())]
657    #[case::multiple_bin(&TRY_MULTIPLE, OutputName::from_static("bin").unwrap())]
658    #[case::default(&Default::default(), OutputName::out())]
659    fn contains_key(#[case] value: &Outputs, #[case] name: OutputName) {
660        assert!(value.contains_key(&name))
661    }
662
663    #[rstest]
664    #[case::multiple_out(&TRY_MULTIPLE, OutputName::out())]
665    fn does_not_contain_key(#[case] value: &Outputs, #[case] name: OutputName) {
666        assert!(!value.contains_key(&name))
667    }
668
669    #[rstest]
670    #[case::single(&SINGLE_OUTPUTS, vec![(OutputName::out(), Default::default())])]
671    #[case::fod(&FOD_OUTPUTS, vec![(OutputName::out(), FOD_OUTPUT.clone())])]
672    #[case::try_fod(&TRY_FOD, vec![(OutputName::out(), FOD_OUTPUT.clone())])]
673    #[case::try_single(&TRY_SINGLE, vec![(OutputName::out(), Default::default())])]
674    #[case::multiple(&TRY_MULTIPLE, vec![
675        (OutputName::from_static("bin").unwrap(), Default::default()),
676        (OutputName::from_static("dev").unwrap(), Default::default()),
677    ])]
678    #[case::default(&Default::default(), vec![(OutputName::out(), Default::default())])]
679    fn iter(#[case] value: &Outputs, #[case] expected: Vec<(OutputName, Output)>) {
680        let actual: Vec<_> = value
681            .iter()
682            .map(|(name, output)| (name.clone(), output.clone()))
683            .collect();
684        assert_eq!(actual, expected);
685    }
686
687    #[rstest]
688    #[case::single(&SINGLE_OUTPUTS, vec![OutputName::out()])]
689    #[case::fod(&FOD_OUTPUTS, vec![OutputName::out()])]
690    #[case::try_fod(&TRY_FOD, vec![OutputName::out()])]
691    #[case::try_single(&TRY_SINGLE, vec![OutputName::out()])]
692    #[case::multiple(&TRY_MULTIPLE, vec![
693        OutputName::from_static("bin").unwrap(),
694        OutputName::from_static("dev").unwrap(),
695    ])]
696    #[case::default(&Default::default(), vec![OutputName::out()])]
697    fn keys(#[case] value: &Outputs, #[case] expected: Vec<OutputName>) {
698        let actual: Vec<_> = value.keys().cloned().collect();
699        assert_eq!(actual, expected);
700    }
701
702    #[rstest]
703    #[case::single(&SINGLE_OUTPUTS, vec![Default::default()])]
704    #[case::fod(&FOD_OUTPUTS, vec![FOD_OUTPUT.clone()])]
705    #[case::try_fod(&TRY_FOD, vec![FOD_OUTPUT.clone()])]
706    #[case::try_single(&TRY_SINGLE, vec![Default::default()])]
707    #[case::multiple(&TRY_MULTIPLE, vec![Default::default(), Default::default()])]
708    #[case::default(&Default::default(), vec![Default::default()])]
709    fn values(#[case] value: &Outputs, #[case] expected: Vec<Output>) {
710        let actual: Vec<_> = value.values().cloned().collect();
711        assert_eq!(actual, expected);
712    }
713}