distro_info/
lib.rs

1//! Parse Debian and Ubuntu distro-info-data files and provide them as easy-to-consume Rust data
2//! structures.
3//!
4//! Use [``UbuntuDistroInfo``](struct.UbuntuDistroInfo.html) to access the Ubuntu data.  (The
5//! Debian implementation has yet to happen.)
6extern crate chrono;
7extern crate csv;
8#[macro_use]
9extern crate failure;
10
11use chrono::naive::NaiveDate;
12use csv::ReaderBuilder;
13use failure::Error;
14
15const UBUNTU_CSV_PATH: &str = "/usr/share/distro-info/ubuntu.csv";
16const DEBIAN_CSV_PATH: &str = "/usr/share/distro-info/debian.csv";
17
18pub enum Distro {
19    Debian,
20    Ubuntu,
21}
22
23impl Distro {
24    pub fn to_string(&self) -> &'static str {
25        match self {
26            Distro::Ubuntu => "Ubuntu",
27            Distro::Debian => "Debian",
28        }
29    }
30}
31
32fn parse_date(field: String) -> Result<NaiveDate, Error> {
33    Ok(NaiveDate::parse_from_str(field.as_str(), "%Y-%m-%d")?)
34}
35
36#[derive(Default, Clone, Debug)]
37pub struct DistroRelease {
38    version: Option<String>,
39    codename: String,
40    series: String,
41    created: Option<NaiveDate>,
42    release: Option<NaiveDate>,
43    eol: Option<NaiveDate>,
44    eol_lts: Option<NaiveDate>,
45    eol_elts: Option<NaiveDate>,
46    eol_esm: Option<NaiveDate>,
47    eol_server: Option<NaiveDate>,
48}
49
50impl DistroRelease {
51    pub fn new(
52        version: String,
53        codename: String,
54        series: String,
55        created: Option<NaiveDate>,
56        release: Option<NaiveDate>,
57        eol: Option<NaiveDate>,
58        eol_lts: Option<NaiveDate>,
59        eol_elts: Option<NaiveDate>,
60        eol_esm: Option<NaiveDate>,
61        eol_server: Option<NaiveDate>,
62    ) -> Self {
63        Self {
64            version: if version.is_empty() {
65                None
66            } else {
67                Some(version)
68            },
69            codename,
70            series,
71            created,
72            release,
73            eol,
74            eol_lts,
75            eol_elts,
76            eol_esm,
77            eol_server,
78        }
79    }
80
81    // Getters
82    pub fn version(&self) -> &Option<String> {
83        &self.version
84    }
85    pub fn codename(&self) -> &String {
86        &self.codename
87    }
88    pub fn series(&self) -> &String {
89        &self.series
90    }
91    pub fn created(&self) -> &Option<NaiveDate> {
92        &self.created
93    }
94    pub fn release(&self) -> &Option<NaiveDate> {
95        &self.release
96    }
97    pub fn eol(&self) -> &Option<NaiveDate> {
98        &self.eol
99    }
100    pub fn eol_server(&self) -> &Option<NaiveDate> {
101        &self.eol_server
102    }
103    pub fn eol_esm(&self) -> &Option<NaiveDate> {
104        &self.eol_esm
105    }
106    pub fn eol_elts(&self) -> &Option<NaiveDate> {
107        &self.eol_elts
108    }
109    pub fn eol_lts(&self) -> &Option<NaiveDate> {
110        &self.eol_lts
111    }
112
113    // Non-getters
114    // TODO(jelmer): This should be Ubuntu-specific; it doesn't apply to Debian releases.
115    pub fn is_lts(&self) -> bool {
116        self.version
117            .as_ref()
118            .map(|version| version.contains("LTS"))
119            .unwrap_or(false)
120    }
121
122    pub fn created_at(&self, date: NaiveDate) -> bool {
123        match self.created {
124            Some(created) => date >= created,
125            None => false,
126        }
127    }
128
129    pub fn released_at(&self, date: NaiveDate) -> bool {
130        match self.release {
131            Some(release) => date >= release,
132            None => false,
133        }
134    }
135
136    pub fn supported_at(&self, date: NaiveDate) -> bool {
137        self.created_at(date)
138            && match self.eol {
139                Some(eol) => match self.eol_server {
140                    Some(eol_server) => date <= ::std::cmp::max(eol, eol_server),
141                    None => date <= eol,
142                },
143                None => true,
144            }
145    }
146}
147
148pub trait DistroInfo: Sized {
149    fn distro(&self) -> &Distro;
150    fn releases(&self) -> &Vec<DistroRelease>;
151    fn from_vec(releases: Vec<DistroRelease>) -> Self;
152    /// The full path to the CSV file to read from for this distro
153    fn csv_path() -> &'static str;
154    /// Read records from the given CSV reader to create a Debian/UbuntuDistroInfo object
155    ///
156    /// (These records must be in the format used in debian.csv/ubuntu.csv as provided by the
157    /// distro-info-data package in Debian/Ubuntu.)
158    fn from_csv_reader<T: std::io::Read>(mut rdr: csv::Reader<T>) -> Result<Self, Error> {
159        let columns = rdr.headers()?.clone();
160        let parse_required_str = |field: Option<String>| -> Result<String, Error> {
161            field.ok_or(format_err!("failed to read required option"))
162        };
163        let getfield = |r: &csv::StringRecord, n: &str| -> Option<String> {
164            columns
165                .iter()
166                .position(|header| header == n)
167                .and_then(|i| r.get(i))
168                .map(|s| s.to_string())
169        };
170        let mut releases = vec![];
171        for record in rdr.records() {
172            let record = record?;
173            releases.push(DistroRelease::new(
174                parse_required_str(getfield(&record, "version"))?,
175                parse_required_str(getfield(&record, "codename"))?,
176                parse_required_str(getfield(&record, "series"))?,
177                getfield(&record, "created").map(parse_date).transpose()?,
178                getfield(&record, "release").map(parse_date).transpose()?,
179                getfield(&record, "eol").map(parse_date).transpose()?,
180                getfield(&record, "eol-lts").map(parse_date).transpose()?,
181                getfield(&record, "eol-elts").map(parse_date).transpose()?,
182                getfield(&record, "eol-esm").map(parse_date).transpose()?,
183                getfield(&record, "eol-server")
184                    .map(parse_date)
185                    .transpose()?,
186            ))
187        }
188        Ok(Self::from_vec(releases))
189    }
190
191    /// Open this distro's CSV file and parse the release data contained therein
192    fn new() -> Result<Self, Error> {
193        Self::from_csv_reader(
194            ReaderBuilder::new()
195                .flexible(true)
196                .has_headers(true)
197                .from_path(Self::csv_path())?,
198        )
199    }
200
201    /// Returns a vector of `DistroRelease`s for releases that had been created at the given date
202    fn all_at(&self, date: NaiveDate) -> Vec<&DistroRelease> {
203        self.releases()
204            .iter()
205            .filter(|distro_release| match distro_release.created {
206                Some(created) => date >= created,
207                None => false,
208            })
209            .collect()
210    }
211
212    /// Returns a vector of `DistroRelease`s for releases that were released at the given date
213    fn released(&self, date: NaiveDate) -> Vec<&DistroRelease> {
214        self.releases()
215            .iter()
216            .filter(|distro_release| distro_release.released_at(date))
217            .collect()
218    }
219
220    /// Returns a vector of `DistroRelease`s for releases that were released and supported at the
221    /// given date
222    fn supported(&self, date: NaiveDate) -> Vec<&DistroRelease> {
223        self.releases()
224            .iter()
225            .filter(|distro_release| distro_release.supported_at(date))
226            .collect()
227    }
228
229    /// Returns a vector of `DistroRelease`s for releases that were released but no longer
230    /// supported at the given date
231    fn unsupported(&self, date: NaiveDate) -> Vec<&DistroRelease> {
232        self.released(date)
233            .into_iter()
234            .filter(|distro_release| !distro_release.supported_at(date))
235            .collect()
236    }
237
238    /// Returns a vector of `DistroRelease`s for releases that were in development at the given
239    /// date
240    fn ubuntu_devel(&self, date: NaiveDate) -> Vec<&DistroRelease> {
241        self.all_at(date)
242            .into_iter()
243            .filter(|distro_release| match distro_release.release {
244                Some(release) => date < release,
245                None => false,
246            })
247            .collect()
248    }
249
250    /// Returns a vector of `DistroRelease`s for releases that were in development at the given
251    /// date
252    fn debian_devel(&self, date: NaiveDate) -> Vec<&DistroRelease> {
253        self.all_at(date)
254            .into_iter()
255            .filter(|distro_release| match distro_release.release {
256                Some(release) => date < release,
257                None => true,
258            })
259            .filter(|distro_release| distro_release.version.is_none())
260            .collect::<Vec<_>>()
261            .first()
262            .copied()
263            .map(|dr| vec![dr])
264            .unwrap_or_else(std::vec::Vec::new)
265    }
266
267    /// Returns a `DistroRelease` for the latest supported, non-EOL release at the given date
268    fn latest(&self, date: NaiveDate) -> Option<&DistroRelease> {
269        self.supported(date)
270            .into_iter()
271            .filter(|distro_release| distro_release.released_at(date))
272            .collect::<Vec<_>>()
273            .last()
274            .copied()
275    }
276
277    fn iter(&self) -> ::std::slice::Iter<DistroRelease> {
278        self.releases().iter()
279    }
280}
281
282pub struct UbuntuDistroInfo {
283    releases: Vec<DistroRelease>,
284}
285
286impl DistroInfo for UbuntuDistroInfo {
287    fn distro(&self) -> &Distro {
288        &Distro::Ubuntu
289    }
290    fn releases(&self) -> &Vec<DistroRelease> {
291        &self.releases
292    }
293    fn csv_path() -> &'static str {
294        UBUNTU_CSV_PATH
295    }
296    /// Initialise an UbuntuDistroInfo struct from a vector of DistroReleases
297    fn from_vec(releases: Vec<DistroRelease>) -> Self {
298        Self { releases }
299    }
300}
301
302impl IntoIterator for UbuntuDistroInfo {
303    type Item = DistroRelease;
304    type IntoIter = ::std::vec::IntoIter<DistroRelease>;
305
306    fn into_iter(self) -> Self::IntoIter {
307        self.releases.into_iter()
308    }
309}
310
311pub struct DebianDistroInfo {
312    releases: Vec<DistroRelease>,
313}
314
315impl DistroInfo for DebianDistroInfo {
316    fn distro(&self) -> &Distro {
317        &Distro::Debian
318    }
319    fn releases(&self) -> &Vec<DistroRelease> {
320        &self.releases
321    }
322    fn csv_path() -> &'static str {
323        DEBIAN_CSV_PATH
324    }
325    /// Initialise an DebianDistroInfo struct from a vector of DistroReleases
326    fn from_vec(releases: Vec<DistroRelease>) -> Self {
327        Self { releases }
328    }
329}
330
331impl IntoIterator for DebianDistroInfo {
332    type Item = DistroRelease;
333    type IntoIter = ::std::vec::IntoIter<DistroRelease>;
334
335    fn into_iter(self) -> Self::IntoIter {
336        self.releases.into_iter()
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use chrono::naive::NaiveDate;
343    use {
344        super::DebianDistroInfo, super::DistroInfo, super::DistroRelease, super::UbuntuDistroInfo,
345    };
346
347    #[test]
348    fn create_struct() {
349        DistroRelease {
350            version: Some("version".to_string()),
351            codename: "codename".to_string(),
352            series: "series".to_string(),
353            created: Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
354            release: Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
355            eol: Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
356            eol_server: Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
357            ..Default::default()
358        };
359    }
360
361    #[test]
362    fn distro_release_new() {
363        let get_date = |mut n| {
364            let mut date = NaiveDate::from_ymd_opt(2018, 6, 14).unwrap();
365            while n > 0 {
366                date = date.succ_opt().unwrap();
367                n -= 1;
368            }
369            date
370        };
371        let distro_release = DistroRelease::new(
372            "version".to_string(),
373            "codename".to_string(),
374            "series".to_string(),
375            Some(get_date(0)),
376            Some(get_date(1)),
377            Some(get_date(2)),
378            Some(get_date(3)),
379            Some(get_date(4)),
380            Some(get_date(5)),
381            Some(get_date(6)),
382        );
383        assert_eq!(Some("version".to_string()), distro_release.version);
384        assert_eq!("codename", distro_release.codename);
385        assert_eq!("series", distro_release.series);
386        assert_eq!(Some(get_date(0)), distro_release.created);
387        assert_eq!(Some(get_date(1)), distro_release.release);
388        assert_eq!(Some(get_date(2)), distro_release.eol);
389        assert_eq!(Some(get_date(3)), distro_release.eol_lts);
390        assert_eq!(Some(get_date(4)), distro_release.eol_elts);
391        assert_eq!(Some(get_date(5)), distro_release.eol_esm);
392        assert_eq!(Some(get_date(6)), distro_release.eol_server);
393
394        assert_eq!(&Some("version".to_string()), distro_release.version());
395        assert_eq!(&"codename", distro_release.codename());
396        assert_eq!(&"series", distro_release.series());
397        assert_eq!(&Some(get_date(0)), distro_release.created());
398        assert_eq!(&Some(get_date(1)), distro_release.release());
399        assert_eq!(&Some(get_date(2)), distro_release.eol());
400        assert_eq!(&Some(get_date(3)), distro_release.eol_lts());
401        assert_eq!(&Some(get_date(4)), distro_release.eol_elts());
402        assert_eq!(&Some(get_date(5)), distro_release.eol_esm());
403        assert_eq!(&Some(get_date(6)), distro_release.eol_server());
404    }
405
406    #[test]
407    fn distro_release_is_lts() {
408        let distro_release = DistroRelease::new(
409            "98.04 LTS".to_string(),
410            "codename".to_string(),
411            "series".to_string(),
412            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
413            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
414            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
415            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
416            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
417            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
418            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
419        );
420        assert!(distro_release.is_lts());
421
422        let distro_release = DistroRelease::new(
423            "98.04".to_string(),
424            "codename".to_string(),
425            "series".to_string(),
426            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
427            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
428            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
429            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
430            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
431            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
432            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
433        );
434        assert!(!distro_release.is_lts());
435    }
436
437    #[test]
438    fn distro_release_released_at() {
439        let distro_release = DistroRelease::new(
440            "98.04 LTS".to_string(),
441            "codename".to_string(),
442            "series".to_string(),
443            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
444            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
445            Some(NaiveDate::from_ymd_opt(2018, 6, 16).unwrap()),
446            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
447            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
448            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
449            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
450        );
451        // not released before release day
452        assert!(!distro_release.released_at(NaiveDate::from_ymd_opt(2018, 6, 13).unwrap()));
453        // released on release day
454        assert!(distro_release.released_at(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()));
455        // still released after EOL
456        assert!(distro_release.released_at(NaiveDate::from_ymd_opt(2018, 6, 17).unwrap()));
457    }
458
459    #[test]
460    fn distro_release_supported_at() {
461        let distro_release = DistroRelease::new(
462            "98.04 LTS".to_string(),
463            "codename".to_string(),
464            "series".to_string(),
465            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
466            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
467            Some(NaiveDate::from_ymd_opt(2018, 6, 16).unwrap()),
468            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
469            Some(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()),
470            None,
471            None,
472        );
473        // not supported before release day
474        assert!(!distro_release.supported_at(NaiveDate::from_ymd_opt(2018, 6, 13).unwrap()));
475        // supported on release day
476        assert!(distro_release.supported_at(NaiveDate::from_ymd_opt(2018, 6, 14).unwrap()));
477        // not supported after EOL
478        assert!(!distro_release.supported_at(NaiveDate::from_ymd_opt(2018, 6, 17).unwrap()));
479    }
480
481    #[test]
482    fn debian_distro_info_new() {
483        DebianDistroInfo::new().unwrap();
484    }
485
486    #[test]
487    fn ubuntu_distro_info_new() {
488        UbuntuDistroInfo::new().unwrap();
489    }
490
491    #[test]
492    fn debian_distro_info_item() {
493        let distro_release = DebianDistroInfo::new().unwrap().into_iter().next().unwrap();
494        assert_eq!(Some("1.1".to_string()), distro_release.version);
495        assert_eq!("Buzz", distro_release.codename);
496        assert_eq!("buzz", distro_release.series);
497        assert_eq!(
498            Some(NaiveDate::from_ymd_opt(1993, 8, 16).unwrap()),
499            distro_release.created
500        );
501        assert_eq!(
502            Some(NaiveDate::from_ymd_opt(1996, 6, 17).unwrap()),
503            distro_release.release
504        );
505        assert_eq!(
506            Some(NaiveDate::from_ymd_opt(1997, 6, 5).unwrap()),
507            distro_release.eol
508        );
509        assert_eq!(None, distro_release.eol_server);
510    }
511
512    #[test]
513    fn ubuntu_distro_info_item() {
514        let distro_release = UbuntuDistroInfo::new().unwrap().into_iter().next().unwrap();
515        assert_eq!(Some("4.10".to_string()), distro_release.version);
516        assert_eq!("Warty Warthog", distro_release.codename);
517        assert_eq!("warty", distro_release.series);
518        assert_eq!(
519            Some(NaiveDate::from_ymd_opt(2004, 3, 5).unwrap()),
520            distro_release.created
521        );
522        assert_eq!(
523            Some(NaiveDate::from_ymd_opt(2004, 10, 20).unwrap()),
524            distro_release.release
525        );
526        assert_eq!(
527            Some(NaiveDate::from_ymd_opt(2006, 4, 30).unwrap()),
528            distro_release.eol
529        );
530        assert_eq!(None, distro_release.eol_server);
531    }
532
533    #[test]
534    fn ubuntu_distro_info_eol_server() {
535        let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
536        for distro_release in ubuntu_distro_info {
537            match distro_release.series.as_ref() {
538                "breezy" => assert_eq!(None, distro_release.eol_server),
539                "dapper" => {
540                    assert_eq!(
541                        Some(NaiveDate::from_ymd_opt(2011, 6, 1).unwrap()),
542                        distro_release.eol_server
543                    );
544                    break;
545                }
546                _ => {}
547            }
548        }
549    }
550    #[test]
551    fn ubuntu_distro_info_released() {
552        let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
553        // Use dapper's release date to confirm we don't have a boundary issue
554        let date = NaiveDate::from_ymd_opt(2006, 6, 1).unwrap();
555        let released_series: Vec<String> = ubuntu_distro_info
556            .released(date)
557            .iter()
558            .map(|distro_release| distro_release.series.clone())
559            .collect();
560        assert_eq!(
561            vec![
562                "warty".to_string(),
563                "hoary".to_string(),
564                "breezy".to_string(),
565                "dapper".to_string(),
566            ],
567            released_series
568        );
569    }
570
571    #[test]
572    fn ubuntu_distro_info_supported() {
573        let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
574        // Use bionic's release date to confirm we don't have a boundary issue
575        let date = NaiveDate::from_ymd_opt(2018, 4, 26).unwrap();
576        let supported_series: Vec<String> = ubuntu_distro_info
577            .supported(date)
578            .iter()
579            .map(|distro_release| distro_release.series.clone())
580            .collect();
581        assert_eq!(
582            vec![
583                "trusty".to_string(),
584                "xenial".to_string(),
585                "artful".to_string(),
586                "bionic".to_string(),
587                "cosmic".to_string(),
588            ],
589            supported_series
590        );
591    }
592
593    #[test]
594    fn ubuntu_distro_info_unsupported() {
595        let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
596        // Use bionic's release date to confirm we don't have a boundary issue
597        let date = NaiveDate::from_ymd_opt(2006, 11, 1).unwrap();
598        let unsupported_series: Vec<String> = ubuntu_distro_info
599            .unsupported(date)
600            .iter()
601            .map(|distro_release| distro_release.series.clone())
602            .collect();
603        assert_eq!(
604            vec!["warty".to_string(), "hoary".to_string()],
605            unsupported_series
606        );
607    }
608
609    #[test]
610    fn ubuntu_distro_info_supported_on_eol_day() {
611        let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
612        // Use artful's EOL date to confirm we don't have a boundary issue
613        let date = NaiveDate::from_ymd_opt(2018, 7, 19).unwrap();
614        let supported_series: Vec<String> = ubuntu_distro_info
615            .supported(date)
616            .iter()
617            .map(|distro_release| distro_release.series.clone())
618            .collect();
619        assert_eq!(
620            vec![
621                "trusty".to_string(),
622                "xenial".to_string(),
623                "artful".to_string(),
624                "bionic".to_string(),
625                "cosmic".to_string(),
626            ],
627            supported_series
628        );
629    }
630
631    #[test]
632    fn ubuntu_distro_info_supported_with_server_eol() {
633        let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
634        let date = NaiveDate::from_ymd_opt(2011, 5, 14).unwrap();
635        let supported_series: Vec<String> = ubuntu_distro_info
636            .supported(date)
637            .iter()
638            .map(|distro_release| distro_release.series.clone())
639            .collect();
640        assert!(supported_series.contains(&"dapper".to_string()));
641    }
642
643    #[test]
644    fn ubuntu_distro_info_devel() {
645        let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
646        let date = NaiveDate::from_ymd_opt(2018, 4, 26).unwrap();
647        let devel_series: Vec<String> = ubuntu_distro_info
648            .ubuntu_devel(date)
649            .iter()
650            .map(|distro_release| distro_release.series.clone())
651            .collect();
652        assert_eq!(vec!["cosmic".to_string()], devel_series);
653    }
654
655    #[test]
656    fn ubuntu_distro_info_all_at() {
657        let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
658        let date = NaiveDate::from_ymd_opt(2005, 4, 8).unwrap();
659        let all_series: Vec<String> = ubuntu_distro_info
660            .all_at(date)
661            .iter()
662            .map(|distro_release| distro_release.series.clone())
663            .collect();
664        assert_eq!(
665            vec![
666                "warty".to_string(),
667                "hoary".to_string(),
668                "breezy".to_string(),
669            ],
670            all_series
671        );
672    }
673
674    #[test]
675    fn ubuntu_distro_info_latest() {
676        let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
677        let date = NaiveDate::from_ymd_opt(2005, 4, 8).unwrap();
678        let latest_series = ubuntu_distro_info.latest(date).unwrap().series.clone();
679        assert_eq!("hoary".to_string(), latest_series);
680    }
681
682    #[test]
683    fn ubuntu_distro_info_iter() {
684        let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
685        let iter_suites: Vec<String> = ubuntu_distro_info
686            .iter()
687            .map(|distro_release| distro_release.series.clone())
688            .collect();
689        let mut for_loop_suites = vec![];
690        for distro_release in ubuntu_distro_info {
691            for_loop_suites.push(distro_release.series.clone());
692        }
693        assert_eq!(for_loop_suites, iter_suites);
694    }
695
696    #[test]
697    fn ubuntu_distro_info_iters_are_separate() {
698        let ubuntu_distro_info = UbuntuDistroInfo::new().unwrap();
699        let mut iter1 = ubuntu_distro_info.iter();
700        let mut iter2 = ubuntu_distro_info.iter();
701        assert_eq!(iter1.next().unwrap().series, iter2.next().unwrap().series);
702    }
703}