pkgcraft/repo/
fake.rs

1use std::hash::{Hash, Hasher};
2use std::sync::Arc;
3use std::{fmt, fs};
4
5use camino::{Utf8Path, Utf8PathBuf};
6use indexmap::{IndexMap, IndexSet};
7use tracing::error;
8
9use crate::Error;
10use crate::config::RepoConfig;
11use crate::dep::{Cpn, Cpv, Dep, Version};
12use crate::pkg::fake::Pkg;
13use crate::restrict::{Restrict, Restriction};
14use crate::traits::Contains;
15use crate::types::OrderedSet;
16
17use super::{PkgRepository, RepoFormat, Repository, make_repo_traits};
18
19type VersionMap = IndexMap<String, IndexSet<Version>>;
20type PkgMap = IndexMap<String, VersionMap>;
21
22#[derive(Clone)]
23struct InternalFakeRepo {
24    id: String,
25    config: RepoConfig,
26    pkgmap: PkgMap,
27    cpvs: OrderedSet<Cpv>,
28}
29
30#[derive(Clone)]
31pub struct FakeRepo(Arc<InternalFakeRepo>);
32
33impl fmt::Debug for FakeRepo {
34    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
35        f.debug_struct("FakeRepo").field("id", &self.id()).finish()
36    }
37}
38
39impl PartialEq for FakeRepo {
40    fn eq(&self, other: &Self) -> bool {
41        self.0.id == other.0.id && self.0.cpvs == other.0.cpvs
42    }
43}
44
45impl Eq for FakeRepo {}
46
47impl Hash for FakeRepo {
48    fn hash<H: Hasher>(&self, state: &mut H) {
49        self.0.id.hash(state);
50        self.0.cpvs.hash(state);
51    }
52}
53
54make_repo_traits!(FakeRepo);
55
56impl FakeRepo {
57    pub fn new(id: &str, priority: i32) -> Self {
58        Self(Arc::new(InternalFakeRepo {
59            id: id.to_string(),
60            config: RepoConfig {
61                priority: Some(priority),
62                ..RepoFormat::Fake.into()
63            },
64            pkgmap: Default::default(),
65            cpvs: Default::default(),
66        }))
67    }
68
69    pub fn pkgs<I>(mut self, iter: I) -> crate::Result<Self>
70    where
71        I: IntoIterator,
72        I::Item: TryInto<Cpv>,
73        <I::Item as TryInto<Cpv>>::Error: fmt::Display,
74    {
75        self.extend(iter)?;
76        Ok(self)
77    }
78
79    pub(crate) fn from_config<S: AsRef<str>>(
80        id: S,
81        config: &RepoConfig,
82    ) -> crate::Result<Self> {
83        let id = id.as_ref();
84        let data = fs::read_to_string(&config.location).map_err(|e| Error::NotARepo {
85            kind: RepoFormat::Fake,
86            id: id.to_string(),
87            err: e.to_string(),
88        })?;
89        let mut repo = Self(Arc::new(InternalFakeRepo {
90            id: id.to_string(),
91            config: config.clone(),
92            pkgmap: Default::default(),
93            cpvs: Default::default(),
94        }));
95        repo.extend(data.lines())?;
96        Ok(repo)
97    }
98
99    pub fn from_path<P: AsRef<Utf8Path>, S: AsRef<str>>(
100        id: S,
101        priority: i32,
102        path: P,
103    ) -> crate::Result<Self> {
104        let id = id.as_ref();
105        let path = path.as_ref();
106        let data = fs::read_to_string(path).map_err(|e| Error::NotARepo {
107            kind: RepoFormat::Fake,
108            id: id.to_string(),
109            err: e.to_string(),
110        })?;
111        let config = RepoConfig {
112            location: Utf8PathBuf::from(path),
113            priority: Some(priority),
114            ..RepoFormat::Fake.into()
115        };
116        let mut repo = Self(Arc::new(InternalFakeRepo {
117            id: id.to_string(),
118            config,
119            pkgmap: Default::default(),
120            cpvs: Default::default(),
121        }));
122        repo.extend(data.lines())?;
123        Ok(repo)
124    }
125
126    pub fn extend<I>(&mut self, iter: I) -> crate::Result<()>
127    where
128        I: IntoIterator,
129        I::Item: TryInto<Cpv>,
130        <I::Item as TryInto<Cpv>>::Error: fmt::Display,
131    {
132        let mut repo = (*self.0).clone();
133        let orig_len = repo.cpvs.len();
134        for s in iter {
135            match s.try_into() {
136                Ok(cpv) => {
137                    repo.cpvs.insert(cpv);
138                }
139                Err(e) => error!("{e}"),
140            }
141        }
142
143        if orig_len != repo.cpvs.len() {
144            repo.cpvs.sort_unstable();
145
146            // recreate entire PkgMap structure to preserve correct ordering
147            let mut pkgmap = PkgMap::new();
148            for cpv in &repo.cpvs {
149                pkgmap
150                    .entry(cpv.category().into())
151                    .or_default()
152                    .entry(cpv.package().into())
153                    .or_default()
154                    .insert(cpv.version().clone());
155            }
156            repo.pkgmap = pkgmap;
157        }
158
159        self.0 = Arc::new(repo);
160        Ok(())
161    }
162
163    /// Retrieve a package from the repo given its [`Cpv`].
164    pub fn get_pkg<T>(&self, value: T) -> crate::Result<Pkg>
165    where
166        T: TryInto<Cpv>,
167        Error: From<T::Error>,
168    {
169        let cpv = value.try_into()?;
170        if self.contains(&cpv) {
171            Ok(Pkg::new(cpv, self.clone()))
172        } else {
173            Err(Error::InvalidValue(format!("not in repo: {cpv}")))
174        }
175    }
176}
177
178impl fmt::Display for FakeRepo {
179    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
180        write!(f, "{}", self.0.id)
181    }
182}
183
184impl PkgRepository for FakeRepo {
185    type Pkg = Pkg;
186    type IterCpn = IterCpn;
187    type IterCpnRestrict = IterCpnRestrict;
188    type IterCpv = IterCpv;
189    type IterCpvRestrict = IterCpvRestrict;
190    type Iter = Iter;
191    type IterRestrict = IterRestrict;
192
193    // TODO: cache categories/packages/versions values in OnceCell fields?
194    fn categories(&self) -> IndexSet<String> {
195        self.0.pkgmap.keys().cloned().collect()
196    }
197
198    fn packages(&self, cat: &str) -> IndexSet<String> {
199        self.0
200            .pkgmap
201            .get(cat)
202            .map(|pkgs| pkgs.keys().cloned().collect())
203            .unwrap_or_default()
204    }
205
206    fn versions(&self, cat: &str, pkg: &str) -> IndexSet<Version> {
207        self.0
208            .pkgmap
209            .get(cat)
210            .and_then(|pkgs| pkgs.get(pkg))
211            .cloned()
212            .unwrap_or_default()
213    }
214
215    fn len(&self) -> usize {
216        self.0.cpvs.len()
217    }
218
219    fn is_empty(&self) -> bool {
220        self.0.cpvs.is_empty()
221    }
222
223    fn iter_cpn(&self) -> Self::IterCpn {
224        IterCpn {
225            iter: self
226                .0
227                .cpvs
228                .iter()
229                .map(|x| x.cpn())
230                .cloned()
231                .collect::<IndexSet<_>>()
232                .into_iter(),
233        }
234    }
235
236    fn iter_cpn_restrict<R: Into<Restrict>>(&self, value: R) -> Self::IterCpnRestrict {
237        IterCpnRestrict {
238            iter: self.iter_cpn(),
239            restrict: value.into(),
240        }
241    }
242
243    fn iter_cpv(&self) -> Self::IterCpv {
244        IterCpv {
245            iter: self.0.cpvs.clone().into_iter(),
246        }
247    }
248
249    fn iter_cpv_restrict<R: Into<Restrict>>(&self, value: R) -> Self::IterCpvRestrict {
250        IterCpvRestrict {
251            iter: self.iter_cpv(),
252            restrict: value.into(),
253        }
254    }
255
256    fn iter(&self) -> Self::Iter {
257        self.into_iter()
258    }
259
260    fn iter_restrict<R: Into<Restrict>>(&self, val: R) -> Self::IterRestrict {
261        IterRestrict {
262            iter: self.into_iter(),
263            restrict: val.into(),
264        }
265    }
266}
267
268impl Contains<&Cpn> for FakeRepo {
269    fn contains(&self, cpn: &Cpn) -> bool {
270        self.iter_restrict(cpn).next().is_some()
271    }
272}
273
274impl Contains<&Cpv> for FakeRepo {
275    fn contains(&self, cpv: &Cpv) -> bool {
276        self.0.cpvs.contains(cpv)
277    }
278}
279
280impl Contains<&Dep> for FakeRepo {
281    fn contains(&self, dep: &Dep) -> bool {
282        self.iter_restrict(dep).next().is_some()
283    }
284}
285
286impl Repository for FakeRepo {
287    fn config(&self) -> &RepoConfig {
288        &self.0.config
289    }
290
291    fn id(&self) -> &str {
292        &self.0.id
293    }
294}
295
296impl IntoIterator for &FakeRepo {
297    type Item = crate::Result<Pkg>;
298    type IntoIter = Iter;
299
300    fn into_iter(self) -> Self::IntoIter {
301        Iter {
302            iter: self.0.cpvs.clone().into_iter(),
303            repo: self.clone(),
304        }
305    }
306}
307
308#[derive(Debug)]
309pub struct IterCpn {
310    iter: indexmap::set::IntoIter<Cpn>,
311}
312
313impl Iterator for IterCpn {
314    type Item = Cpn;
315
316    fn next(&mut self) -> Option<Self::Item> {
317        self.iter.next()
318    }
319}
320
321#[derive(Debug)]
322pub struct IterCpnRestrict {
323    iter: IterCpn,
324    restrict: Restrict,
325}
326
327impl Iterator for IterCpnRestrict {
328    type Item = Cpn;
329
330    fn next(&mut self) -> Option<Self::Item> {
331        self.iter.find(|cpn| self.restrict.matches(cpn))
332    }
333}
334
335#[derive(Debug)]
336pub struct IterCpv {
337    iter: indexmap::set::IntoIter<Cpv>,
338}
339
340impl Iterator for IterCpv {
341    type Item = Cpv;
342
343    fn next(&mut self) -> Option<Self::Item> {
344        self.iter.next()
345    }
346}
347
348#[derive(Debug)]
349pub struct IterCpvRestrict {
350    iter: IterCpv,
351    restrict: Restrict,
352}
353
354impl Iterator for IterCpvRestrict {
355    type Item = Cpv;
356
357    fn next(&mut self) -> Option<Self::Item> {
358        self.iter.find(|cpv| self.restrict.matches(cpv))
359    }
360}
361
362#[derive(Debug)]
363pub struct Iter {
364    iter: indexmap::set::IntoIter<Cpv>,
365    repo: FakeRepo,
366}
367
368impl Iterator for Iter {
369    type Item = crate::Result<Pkg>;
370
371    fn next(&mut self) -> Option<Self::Item> {
372        self.iter
373            .next()
374            .map(|cpv| Ok(Pkg::new(cpv, self.repo.clone())))
375    }
376}
377
378#[derive(Debug)]
379pub struct IterRestrict {
380    iter: Iter,
381    restrict: Restrict,
382}
383
384impl Iterator for IterRestrict {
385    type Item = crate::Result<Pkg>;
386
387    fn next(&mut self) -> Option<Self::Item> {
388        self.iter.find_map(|r| match r {
389            Ok(pkg) if self.restrict.matches(&pkg) => Some(Ok(pkg)),
390            Ok(_) => None,
391            Err(e) => unreachable!("invalid fake pkg: {e}"),
392        })
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use std::fs::File;
399    use std::io::Write;
400
401    use itertools::Itertools;
402    use tempfile::tempdir;
403    use tracing_test::traced_test;
404
405    use crate::pkg::Package;
406    use crate::test::*;
407
408    use super::*;
409
410    #[test]
411    fn from_path() {
412        let dir = tempdir().unwrap();
413        let path = Utf8Path::from_path(dir.path()).unwrap();
414
415        // empty dir
416        assert!(FakeRepo::from_path("test", 0, path).is_err());
417
418        // empty file
419        let repo_path = path.join("fake");
420        let mut f = File::create(&repo_path).unwrap();
421        let repo = FakeRepo::from_path("test", 0, &repo_path).unwrap();
422        assert!(repo.is_empty());
423
424        // non-empty file
425        writeln!(&mut f, "cat/pkg-1").unwrap();
426        let repo = FakeRepo::from_path("test", 0, &repo_path).unwrap();
427        assert_ordered_eq!(repo.iter_cpv().map(|x| x.to_string()), ["cat/pkg-1"]);
428    }
429
430    #[test]
431    fn repository_trait() {
432        let repo = FakeRepo::new("fake", 0);
433        assert_eq!(repo.format(), RepoFormat::Fake);
434        assert_eq!(repo.id(), "fake");
435        assert_eq!(repo.priority(), 0);
436        assert_eq!(repo.path(), "");
437        assert!(repo.sync().is_ok());
438    }
439
440    #[test]
441    fn categories() {
442        let mut repo = FakeRepo::new("fake", 0);
443        // empty repo
444        assert!(repo.categories().is_empty());
445        // existing pkgs
446        repo.extend(["cat1/pkg-a-1", "cat1/pkg-a-2", "cat2/pkg-b-3"])
447            .unwrap();
448        assert_ordered_eq!(repo.categories(), ["cat1", "cat2"])
449    }
450
451    #[test]
452    fn packages() {
453        let mut repo: FakeRepo;
454        // empty repo
455        repo = FakeRepo::new("fake", 0);
456        assert!(repo.packages("cat").is_empty());
457        // existing pkgs
458        repo.extend(["cat1/pkg-a-1", "cat1/pkg-a-2", "cat2/pkg-b-3"])
459            .unwrap();
460        assert!(repo.packages("cat").is_empty());
461        assert_ordered_eq!(repo.packages("cat1"), ["pkg-a"]);
462        assert_ordered_eq!(repo.packages("cat2"), ["pkg-b"]);
463    }
464
465    #[test]
466    fn versions() {
467        let ver = |s: &str| Version::try_new(s).unwrap();
468        let mut repo: FakeRepo;
469        // empty repo
470        repo = FakeRepo::new("fake", 0);
471        assert!(repo.versions("cat", "pkg").is_empty());
472        // existing pkgs
473        repo.extend(["cat1/pkg-a-1", "cat1/pkg-a-2", "cat2/pkg-b-3"])
474            .unwrap();
475        assert!(repo.versions("cat", "pkg").is_empty());
476        assert_ordered_eq!(repo.versions("cat1", "pkg-a"), [ver("1"), ver("2")]);
477        assert_ordered_eq!(repo.versions("cat2", "pkg-b"), [ver("3")]);
478    }
479
480    #[test]
481    fn len() {
482        let mut repo = FakeRepo::new("fake", 0);
483        assert_eq!(repo.len(), 0);
484        repo.extend(["cat/pkg-0"]).unwrap();
485        assert_eq!(repo.len(), 1);
486        repo.extend(["cat/pkg-0", "cat1/pkg1-1", "cat2/pkg2-2"])
487            .unwrap();
488        assert_eq!(repo.len(), 3);
489    }
490
491    #[traced_test]
492    #[test]
493    fn extend() {
494        let mut repo = FakeRepo::new("fake", 0).pkgs(["cat/pkg-2"]).unwrap();
495        let pkgs: Vec<_> = repo.iter().try_collect().unwrap();
496        assert_ordered_eq!(pkgs.iter().map(|x| x.cpv().to_string()), ["cat/pkg-2"]);
497
498        // add valid cpv
499        repo.extend(["cat/pkg-0"]).unwrap();
500        let pkgs: Vec<_> = repo.iter().try_collect().unwrap();
501        assert_ordered_eq!(
502            pkgs.iter().map(|x| x.cpv().to_string()),
503            ["cat/pkg-0", "cat/pkg-2"]
504        );
505
506        // add multiple cpvs, invalid cpvs logged and ignored
507        repo.extend(["cat/pkg-3", "cat/pkg", "cat/pkg-1", "a/b-0"])
508            .unwrap();
509        let pkgs: Vec<_> = repo.iter().try_collect().unwrap();
510        assert_ordered_eq!(
511            pkgs.iter().map(|x| x.cpv().to_string()),
512            ["a/b-0", "cat/pkg-0", "cat/pkg-1", "cat/pkg-2", "cat/pkg-3"]
513        );
514        assert_logs_re!("invalid cpv: cat/pkg");
515
516        // re-add existing cpvs
517        repo.extend(["cat/pkg-3", "cat/pkg-1", "a/b-0"]).unwrap();
518        let pkgs: Vec<_> = repo.iter().try_collect().unwrap();
519        assert_ordered_eq!(
520            pkgs.iter().map(|x| x.cpv().to_string()),
521            ["a/b-0", "cat/pkg-0", "cat/pkg-1", "cat/pkg-2", "cat/pkg-3"]
522        );
523    }
524
525    #[test]
526    fn contains() {
527        let repo = FakeRepo::new("fake", 0).pkgs(["cat/pkg-1"]).unwrap();
528
529        // path is always false due to fake repo
530        assert!(!repo.contains("cat/pkg"));
531
532        // Cpn
533        let cpn = Cpn::try_new("cat/pkg").unwrap();
534        assert!(repo.contains(&cpn));
535        let cpn = Cpn::try_new("a/pkg").unwrap();
536        assert!(!repo.contains(&cpn));
537
538        // Cpv
539        let cpv = Cpv::try_new("cat/pkg-1").unwrap();
540        assert!(repo.contains(&cpv));
541        let cpv = Cpv::try_new("cat/pkg-2").unwrap();
542        assert!(!repo.contains(&cpv));
543
544        // Dep
545        let dep = Dep::try_new("cat/pkg::fake").unwrap();
546        assert!(repo.contains(&dep));
547        let dep = Dep::try_new("cat/pkg::repo").unwrap();
548        assert!(!repo.contains(&dep));
549        let dep = Dep::try_new("=cat/pkg-1").unwrap();
550        assert!(repo.contains(&dep));
551        let dep = Dep::try_new(">cat/pkg-1").unwrap();
552        assert!(!repo.contains(&dep));
553
554        // Restrict
555        assert!(repo.contains(&Restrict::True));
556        assert!(!repo.contains(&Restrict::False));
557        let restrict = Restrict::from(Cpn::try_new("cat/pkg").unwrap());
558        assert!(repo.contains(&restrict));
559        let restrict = Restrict::from(Cpv::try_new("cat/pkg-1").unwrap());
560        assert!(repo.contains(&restrict));
561    }
562
563    #[test]
564    fn iter() {
565        let repo = FakeRepo::new("fake", 0)
566            .pkgs(["cat/pkg-0", "acat/bpkg-1"])
567            .unwrap();
568        let pkgs: Vec<_> = repo.iter().try_collect().unwrap();
569        assert_ordered_eq!(
570            pkgs.iter().map(|x| x.cpv().to_string()),
571            ["acat/bpkg-1", "cat/pkg-0"]
572        );
573    }
574
575    #[test]
576    fn iter_cpn() {
577        let cpvs: Vec<_> = (0..100)
578            .map(|x| Cpv::try_new(format!("cat/pkg-{x}")).unwrap())
579            .collect();
580        let repo = FakeRepo::new("fake", 0).pkgs(&cpvs).unwrap();
581        let cpn = Cpn::try_new("cat/pkg").unwrap();
582        assert_ordered_eq!(repo.iter_cpn(), [cpn]);
583    }
584
585    #[test]
586    fn iter_cpn_restrict() {
587        let cpvs: Vec<_> = (0..100)
588            .map(|x| Cpv::try_new(format!("cat/pkg-{x}")).unwrap())
589            .collect();
590        let repo = FakeRepo::new("fake", 0).pkgs(&cpvs).unwrap();
591        let cpn = Cpn::try_new("a/b").unwrap();
592        assert!(repo.iter_cpn_restrict(&cpn).next().is_none());
593        let cpv = Cpv::try_new("cat/pkg-1").unwrap();
594        assert!(repo.iter_cpn_restrict(&cpv).next().is_none());
595        let cpn = Cpn::try_new("cat/pkg").unwrap();
596        assert_ordered_eq!(repo.iter_cpn_restrict(&cpn), [cpn]);
597    }
598
599    #[test]
600    fn iter_cpv() {
601        let cpvs: Vec<_> = (0..100)
602            .map(|x| Cpv::try_new(format!("cat/pkg-{x}")).unwrap())
603            .collect();
604        let repo = FakeRepo::new("fake", 0).pkgs(&cpvs).unwrap();
605        assert_ordered_eq!(repo.iter_cpv(), cpvs);
606    }
607
608    #[test]
609    fn iter_cpv_restrict() {
610        let cpvs: Vec<_> = (0..100)
611            .map(|x| Cpv::try_new(format!("cat/pkg-{x}")).unwrap())
612            .collect();
613        let repo = FakeRepo::new("fake", 0).pkgs(&cpvs).unwrap();
614        let cpv = Cpv::try_new("cat/pkg-1").unwrap();
615        assert_ordered_eq!(repo.iter_cpv_restrict(&cpv), [cpv]);
616        let cpn = Cpn::try_new("a/b").unwrap();
617        assert!(repo.iter_cpv_restrict(&cpn).next().is_none());
618        let cpn = Cpn::try_new("cat/pkg").unwrap();
619        assert_ordered_eq!(repo.iter_cpv_restrict(&cpn), cpvs);
620    }
621
622    #[test]
623    fn get_pkg() {
624        let repo = FakeRepo::new("fake", 0).pkgs(["cat/pkg-1"]).unwrap();
625        // existent
626        assert!(repo.get_pkg("cat/pkg-1").is_ok());
627        // nonexistent
628        assert!(repo.get_pkg("cat/pkg-2").is_err());
629        // invalid Cpv
630        assert!(repo.get_pkg("cat/pkg").is_err());
631    }
632}