radicle/cob/patch/
cache.rs

1use std::ops::ControlFlow;
2use std::str::FromStr;
3
4use sqlite as sql;
5use thiserror::Error;
6
7use crate::cob;
8use crate::cob::cache::{self, StoreReader};
9use crate::cob::cache::{Remove, StoreWriter, Update};
10use crate::cob::store;
11use crate::cob::{Label, ObjectId, TypeName};
12use crate::git;
13use crate::node::device::Device;
14use crate::prelude::RepoId;
15use crate::storage::{HasRepoId, ReadRepository, RepositoryError, SignRepository, WriteRepository};
16
17use super::{
18    ByRevision, MergeTarget, NodeId, Patch, PatchCounts, PatchId, PatchMut, Revision, RevisionId,
19    State, Status,
20};
21
22/// A set of read-only methods for a [`Patch`] store.
23pub trait Patches {
24    type Error: std::error::Error + Send + Sync + 'static;
25
26    /// An iterator for returning a set of patches from the store.
27    type Iter<'a>: Iterator<Item = Result<(PatchId, Patch), Self::Error>> + 'a
28    where
29        Self: 'a;
30
31    /// Get the `Patch`, identified by `id`, returning `None` if it
32    /// was not found.
33    fn get(&self, id: &PatchId) -> Result<Option<Patch>, Self::Error>;
34
35    /// Get the `Patch` and its `Revision`, identified by the revision
36    /// `id`, returning `None` if it was not found.
37    fn find_by_revision(&self, id: &RevisionId) -> Result<Option<ByRevision>, Self::Error>;
38
39    /// List all patches that are in the store.
40    fn list(&self) -> Result<Self::Iter<'_>, Self::Error>;
41
42    /// List all patches in the store that match the provided
43    /// `status`.
44    ///
45    /// Also see [`Patches::opened`], [`Patches::archived`],
46    /// [`Patches::drafted`], [`Patches::merged`].
47    fn list_by_status(&self, status: &Status) -> Result<Self::Iter<'_>, Self::Error>;
48
49    /// Get the [`PatchCounts`] of all the patches in the store.
50    fn counts(&self) -> Result<PatchCounts, Self::Error>;
51
52    /// List all opened patches in the store.
53    fn opened(&self) -> Result<Self::Iter<'_>, Self::Error> {
54        self.list_by_status(&Status::Open)
55    }
56
57    /// List all archived patches in the store.
58    fn archived(&self) -> Result<Self::Iter<'_>, Self::Error> {
59        self.list_by_status(&Status::Archived)
60    }
61
62    /// List all drafted patches in the store.
63    fn drafted(&self) -> Result<Self::Iter<'_>, Self::Error> {
64        self.list_by_status(&Status::Draft)
65    }
66
67    /// List all merged patches in the store.
68    fn merged(&self) -> Result<Self::Iter<'_>, Self::Error> {
69        self.list_by_status(&Status::Merged)
70    }
71
72    /// Returns `true` if there are no patches in the store.
73    fn is_empty(&self) -> Result<bool, Self::Error> {
74        Ok(self.counts()?.total() == 0)
75    }
76}
77
78/// [`Patches`] store that can also [`Update`] and [`Remove`]
79/// [`Patch`] in/from the store.
80pub trait PatchesMut: Patches + Update<Patch> + Remove<Patch> {}
81
82impl<T> PatchesMut for T where T: Patches + Update<Patch> + Remove<Patch> {}
83
84/// A `Patch` store that relies on the `cache` for reads and as a
85/// write-through cache.
86///
87/// The `store` is used for the main storage when performing a
88/// write-through. It is also used for identifying which `RepoId` is
89/// being used for the `cache`.
90pub struct Cache<R, C> {
91    pub(super) store: R,
92    pub(super) cache: C,
93}
94
95impl<R, C> Cache<R, C> {
96    pub fn new(store: R, cache: C) -> Self {
97        Self { store, cache }
98    }
99
100    pub fn rid(&self) -> RepoId
101    where
102        R: HasRepoId,
103    {
104        self.store.rid()
105    }
106}
107
108impl<'a, R, C> Cache<super::Patches<'a, R>, C> {
109    /// Create a new [`Patch`] using the [`super::Patches`] as the
110    /// main storage, and writing the update to the `cache`.
111    pub fn create<'g, G>(
112        &'g mut self,
113        title: cob::Title,
114        description: impl ToString,
115        target: MergeTarget,
116        base: impl Into<git::Oid>,
117        oid: impl Into<git::Oid>,
118        labels: &[Label],
119        signer: &Device<G>,
120    ) -> Result<PatchMut<'a, 'g, R, C>, super::Error>
121    where
122        R: WriteRepository + cob::Store<Namespace = NodeId>,
123        G: crypto::signature::Signer<crypto::Signature>,
124        C: Update<Patch>,
125    {
126        self.store.create(
127            title,
128            description,
129            target,
130            base,
131            oid,
132            labels,
133            &mut self.cache,
134            signer,
135        )
136    }
137
138    /// Create a new [`Patch`], in a draft state, using the
139    /// [`super::Patches`] as the main storage, and writing the update
140    /// to the `cache`.
141    pub fn draft<'g, G>(
142        &'g mut self,
143        title: cob::Title,
144        description: impl ToString,
145        target: MergeTarget,
146        base: impl Into<git::Oid>,
147        oid: impl Into<git::Oid>,
148        labels: &[Label],
149        signer: &Device<G>,
150    ) -> Result<PatchMut<'a, 'g, R, C>, super::Error>
151    where
152        R: WriteRepository + cob::Store<Namespace = NodeId>,
153        G: crypto::signature::Signer<crypto::Signature>,
154        C: Update<Patch>,
155    {
156        self.store.draft(
157            title,
158            description,
159            target,
160            base,
161            oid,
162            labels,
163            &mut self.cache,
164            signer,
165        )
166    }
167
168    /// Remove the given `id` from the [`super::Patches`] storage, and
169    /// removing the entry from the `cache`.
170    pub fn remove<G>(&mut self, id: &PatchId, signer: &Device<G>) -> Result<(), super::Error>
171    where
172        G: crypto::signature::Signer<crypto::Signature>,
173        R: ReadRepository + SignRepository + cob::Store<Namespace = NodeId>,
174        C: Remove<Patch>,
175    {
176        self.store.remove(id, signer)?;
177        self.cache
178            .remove(id)
179            .map_err(|e| super::Error::CacheRemove {
180                id: *id,
181                err: e.into(),
182            })?;
183        Ok(())
184    }
185
186    /// Read the given `id` from the [`super::Patches`] store and
187    /// writing it to the `cache`.
188    pub fn write(&mut self, id: &PatchId) -> Result<(), super::Error>
189    where
190        R: ReadRepository + cob::Store,
191        C: Update<Patch>,
192    {
193        let issue = self
194            .store
195            .get(id)?
196            .ok_or_else(|| store::Error::NotFound((*super::TYPENAME).clone(), *id))?;
197        self.update(&self.rid(), id, &issue)
198            .map_err(|e| super::Error::CacheUpdate {
199                id: *id,
200                err: e.into(),
201            })?;
202        Ok(())
203    }
204
205    /// Read all the patches from the [`super::Patches`] store and
206    /// writing them to `cache`.
207    ///
208    /// The `callback` is used for reporting success, failures, and
209    /// progress to the caller. The caller may also decide to continue
210    /// or break from the process.
211    pub fn write_all(
212        &mut self,
213        callback: impl Fn(&Result<(PatchId, Patch), store::Error>, &cache::Progress) -> ControlFlow<()>,
214    ) -> Result<(), super::Error>
215    where
216        R: ReadRepository + cob::Store,
217        C: Update<Patch> + Remove<Patch>,
218    {
219        // Start by clearing the cache. This will get rid of patches that are cached but
220        // no longer exist in storage.
221        self.remove_all(&self.rid())
222            .map_err(|e| super::Error::CacheRemoveAll { err: e.into() })?;
223
224        let patches = self.store.all()?;
225        let mut progress = cache::Progress::new(patches.len());
226        for patch in self.store.all()? {
227            progress.inc();
228            match callback(&patch, &progress) {
229                ControlFlow::Continue(()) => match patch {
230                    Ok((id, patch)) => {
231                        self.update(&self.rid(), &id, &patch)
232                            .map_err(|e| super::Error::CacheUpdate { id, err: e.into() })?;
233                    }
234                    Err(_) => continue,
235                },
236                ControlFlow::Break(()) => break,
237            }
238        }
239        Ok(())
240    }
241}
242
243impl<R> Cache<R, StoreReader> {
244    pub fn reader(store: R, cache: StoreReader) -> Self {
245        Self { store, cache }
246    }
247}
248
249impl<R> Cache<R, StoreWriter> {
250    pub fn open(store: R, cache: StoreWriter) -> Self {
251        Self { store, cache }
252    }
253}
254
255impl<'a, R> Cache<super::Patches<'a, R>, StoreWriter>
256where
257    R: ReadRepository + cob::Store,
258{
259    /// Get the [`PatchMut`], identified by `id`, using the
260    /// `StoreWriter` for retrieving the `Patch`.
261    pub fn get_mut<'g>(
262        &'g mut self,
263        id: &ObjectId,
264    ) -> Result<PatchMut<'a, 'g, R, StoreWriter>, Error> {
265        let patch = Patches::get(self, id)?
266            .ok_or_else(move || Error::NotFound(super::TYPENAME.clone(), *id))?;
267
268        Ok(PatchMut {
269            id: *id,
270            patch,
271            store: &mut self.store,
272            cache: &mut self.cache,
273        })
274    }
275}
276
277impl<'a, R> Cache<super::Patches<'a, R>, cache::NoCache>
278where
279    R: ReadRepository + cob::Store<Namespace = NodeId>,
280{
281    /// Get a `Cache` that does no write-through modifications and
282    /// uses the [`super::Patches`] store for all reads and writes.
283    pub fn no_cache(repository: &'a R) -> Result<Self, RepositoryError> {
284        let store = super::Patches::open(repository)?;
285        Ok(Self {
286            store,
287            cache: cache::NoCache,
288        })
289    }
290
291    /// Get the [`PatchMut`], identified by `id`.
292    pub fn get_mut<'g>(
293        &'g mut self,
294        id: &ObjectId,
295    ) -> Result<PatchMut<'a, 'g, R, cache::NoCache>, super::Error> {
296        let patch = self
297            .store
298            .get(id)?
299            .ok_or_else(move || store::Error::NotFound(super::TYPENAME.clone(), *id))?;
300
301        Ok(PatchMut {
302            id: *id,
303            patch,
304            store: &mut self.store,
305            cache: &mut self.cache,
306        })
307    }
308}
309
310impl<R, C> cache::Update<Patch> for Cache<R, C>
311where
312    C: cache::Update<Patch>,
313{
314    type Out = <C as cache::Update<Patch>>::Out;
315    type UpdateError = <C as cache::Update<Patch>>::UpdateError;
316
317    fn update(
318        &mut self,
319        rid: &RepoId,
320        id: &radicle_cob::ObjectId,
321        object: &Patch,
322    ) -> Result<Self::Out, Self::UpdateError> {
323        self.cache.update(rid, id, object)
324    }
325}
326
327impl<R, C> cache::Remove<Patch> for Cache<R, C>
328where
329    C: cache::Remove<Patch>,
330{
331    type Out = <C as cache::Remove<Patch>>::Out;
332    type RemoveError = <C as cache::Remove<Patch>>::RemoveError;
333
334    fn remove(&mut self, id: &ObjectId) -> Result<Self::Out, Self::RemoveError> {
335        self.cache.remove(id)
336    }
337
338    fn remove_all(&mut self, rid: &RepoId) -> Result<Self::Out, Self::RemoveError> {
339        self.cache.remove_all(rid)
340    }
341}
342
343#[derive(Debug, Error)]
344pub enum UpdateError {
345    #[error(transparent)]
346    Json(#[from] serde_json::Error),
347    #[error(transparent)]
348    Sql(#[from] sql::Error),
349}
350
351impl Update<Patch> for StoreWriter {
352    type Out = bool;
353    type UpdateError = UpdateError;
354
355    fn update(
356        &mut self,
357        rid: &RepoId,
358        id: &ObjectId,
359        object: &Patch,
360    ) -> Result<Self::Out, Self::UpdateError> {
361        let mut stmt = self.db.prepare(
362            "INSERT INTO patches (id, repo, patch)
363             VALUES (?1, ?2, ?3)
364             ON CONFLICT DO UPDATE
365             SET patch = (?3)",
366        )?;
367
368        stmt.bind((1, sql::Value::String(id.to_string())))?;
369        stmt.bind((2, rid))?;
370        stmt.bind((3, sql::Value::String(serde_json::to_string(&object)?)))?;
371        stmt.next()?;
372
373        Ok(self.db.change_count() > 0)
374    }
375}
376
377impl Remove<Patch> for StoreWriter {
378    type Out = bool;
379    type RemoveError = sql::Error;
380
381    fn remove(&mut self, id: &ObjectId) -> Result<Self::Out, Self::RemoveError> {
382        let mut stmt = self.db.prepare(
383            "DELETE FROM patches
384             WHERE id = ?1",
385        )?;
386
387        stmt.bind((1, sql::Value::String(id.to_string())))?;
388        stmt.next()?;
389
390        Ok(self.db.change_count() > 0)
391    }
392
393    fn remove_all(&mut self, rid: &RepoId) -> Result<Self::Out, Self::RemoveError> {
394        let mut stmt = self.db.prepare(
395            "DELETE FROM patches
396             WHERE repo = ?1",
397        )?;
398
399        stmt.bind((1, rid))?;
400        stmt.next()?;
401
402        Ok(self.db.change_count() > 0)
403    }
404}
405
406#[derive(Debug, Error)]
407pub enum Error {
408    #[error("object `{1}` of type `{0}` was not found")]
409    NotFound(TypeName, ObjectId),
410    #[error(transparent)]
411    ObjectId(#[from] cob::object::ParseObjectId),
412    #[error("object {0} failed to parse: {1}")]
413    Object(ObjectId, serde_json::Error),
414    #[error(transparent)]
415    Json(#[from] serde_json::Error),
416    #[error(transparent)]
417    Sql(#[from] sql::Error),
418}
419
420/// Iterator that returns a set of patches based on an SQL query.
421///
422/// The query is expected to return rows with columns identified by
423/// the `id` and `patch` names.
424pub struct PatchesIter<'a> {
425    inner: sql::CursorWithOwnership<'a>,
426}
427
428impl PatchesIter<'_> {
429    fn parse_row(row: sql::Row) -> Result<(PatchId, Patch), Error> {
430        let id = PatchId::from_str(row.try_read::<&str, _>("id")?)?;
431        let patch = serde_json::from_str::<Patch>(row.try_read::<&str, _>("patch")?)
432            .map_err(|e| Error::Object(id, e))?;
433        Ok((id, patch))
434    }
435}
436
437impl Iterator for PatchesIter<'_> {
438    type Item = Result<(PatchId, Patch), Error>;
439
440    fn next(&mut self) -> Option<Self::Item> {
441        let row = self.inner.next()?;
442        Some(row.map_err(Error::from).and_then(PatchesIter::parse_row))
443    }
444}
445
446impl<R> Patches for Cache<R, StoreReader>
447where
448    R: HasRepoId,
449{
450    type Error = Error;
451    type Iter<'b>
452        = PatchesIter<'b>
453    where
454        Self: 'b;
455
456    fn get(&self, id: &PatchId) -> Result<Option<Patch>, Self::Error> {
457        query::get(&self.cache.db, &self.rid(), id)
458    }
459
460    fn find_by_revision(&self, id: &RevisionId) -> Result<Option<ByRevision>, Error> {
461        query::find_by_revision(&self.cache.db, &self.rid(), id)
462    }
463
464    fn list(&self) -> Result<Self::Iter<'_>, Self::Error> {
465        query::list(&self.cache.db, &self.rid())
466    }
467
468    fn list_by_status(&self, status: &Status) -> Result<Self::Iter<'_>, Self::Error> {
469        query::list_by_status(&self.cache.db, &self.rid(), status)
470    }
471
472    fn counts(&self) -> Result<PatchCounts, Self::Error> {
473        query::counts(&self.cache.db, &self.rid())
474    }
475}
476
477pub struct NoCacheIter<'a> {
478    inner: Box<dyn Iterator<Item = Result<(PatchId, Patch), super::Error>> + 'a>,
479}
480
481impl Iterator for NoCacheIter<'_> {
482    type Item = Result<(PatchId, Patch), super::Error>;
483
484    fn next(&mut self) -> Option<Self::Item> {
485        self.inner.next()
486    }
487}
488
489impl<R> Patches for Cache<super::Patches<'_, R>, cache::NoCache>
490where
491    R: ReadRepository + cob::Store<Namespace = NodeId>,
492{
493    type Error = super::Error;
494    type Iter<'b>
495        = NoCacheIter<'b>
496    where
497        Self: 'b;
498
499    fn get(&self, id: &PatchId) -> Result<Option<Patch>, Self::Error> {
500        self.store.get(id).map_err(super::Error::from)
501    }
502
503    fn find_by_revision(&self, id: &RevisionId) -> Result<Option<ByRevision>, Self::Error> {
504        self.store.find_by_revision(id)
505    }
506
507    fn list(&self) -> Result<Self::Iter<'_>, Self::Error> {
508        self.store
509            .all()
510            .map(|inner| NoCacheIter {
511                inner: Box::new(inner.into_iter().map(|res| res.map_err(super::Error::from))),
512            })
513            .map_err(super::Error::from)
514    }
515
516    fn list_by_status(&self, status: &Status) -> Result<Self::Iter<'_>, Self::Error> {
517        let status = *status;
518        self.store
519            .all()
520            .map(move |inner| NoCacheIter {
521                inner: Box::new(inner.into_iter().filter_map(move |res| {
522                    match res {
523                        Ok((id, patch)) => (status == Status::from(&patch.state))
524                            .then_some((id, patch))
525                            .map(Ok),
526                        Err(e) => Some(Err(e.into())),
527                    }
528                })),
529            })
530            .map_err(super::Error::from)
531    }
532
533    fn counts(&self) -> Result<PatchCounts, Self::Error> {
534        self.store.counts().map_err(super::Error::from)
535    }
536}
537
538impl<R> Patches for Cache<R, StoreWriter>
539where
540    R: HasRepoId,
541{
542    type Error = Error;
543    type Iter<'b>
544        = PatchesIter<'b>
545    where
546        Self: 'b;
547
548    fn get(&self, id: &PatchId) -> Result<Option<Patch>, Self::Error> {
549        query::get(&self.cache.db, &self.rid(), id)
550    }
551
552    fn find_by_revision(&self, id: &RevisionId) -> Result<Option<ByRevision>, Error> {
553        query::find_by_revision(&self.cache.db, &self.rid(), id)
554    }
555
556    fn list(&self) -> Result<Self::Iter<'_>, Self::Error> {
557        query::list(&self.cache.db, &self.rid())
558    }
559
560    fn list_by_status(&self, status: &Status) -> Result<Self::Iter<'_>, Self::Error> {
561        query::list_by_status(&self.cache.db, &self.rid(), status)
562    }
563
564    fn counts(&self) -> Result<PatchCounts, Self::Error> {
565        query::counts(&self.cache.db, &self.rid())
566    }
567}
568
569/// Helper SQL queries for [ `Patches`] trait implementations.
570mod query {
571    use sqlite as sql;
572
573    use crate::patch::Status;
574
575    use super::*;
576
577    pub(super) fn get(
578        db: &sql::ConnectionThreadSafe,
579        rid: &RepoId,
580        id: &PatchId,
581    ) -> Result<Option<Patch>, Error> {
582        let key = sql::Value::String(id.to_string());
583        let mut stmt = db.prepare(
584            "SELECT patch
585             FROM patches
586             WHERE id = ?1 AND repo = ?2",
587        )?;
588
589        stmt.bind((1, key))?;
590        stmt.bind((2, rid))?;
591
592        match stmt.into_iter().next().transpose()? {
593            None => Ok(None),
594            Some(row) => {
595                let patch = row.try_read::<&str, _>("patch")?;
596                let patch = serde_json::from_str(patch).map_err(|e| Error::Object(*id, e))?;
597                Ok(Some(patch))
598            }
599        }
600    }
601
602    pub(super) fn find_by_revision(
603        db: &sql::ConnectionThreadSafe,
604        rid: &RepoId,
605        id: &RevisionId,
606    ) -> Result<Option<ByRevision>, Error> {
607        let revision_id = *id;
608        let mut stmt = db.prepare(
609            "SELECT patches.id, patch, revisions.value AS revision
610             FROM patches, json_tree(patches.patch, '$.revisions') AS revisions
611             WHERE repo = ?1
612             AND revisions.key = ?2
613            ",
614        )?;
615        stmt.bind((1, rid))?;
616        stmt.bind((2, sql::Value::String(id.to_string())))?;
617
618        match stmt.into_iter().next().transpose()? {
619            None => Ok(None),
620            Some(row) => {
621                let id = PatchId::from_str(row.try_read::<&str, _>("id")?)?;
622                let patch = serde_json::from_str::<Patch>(row.try_read::<&str, _>("patch")?)
623                    .map_err(|e| Error::Object(id, e))?;
624                let revision =
625                    serde_json::from_str::<Revision>(row.try_read::<&str, _>("revision")?)?;
626                Ok(Some(ByRevision {
627                    id,
628                    patch,
629                    revision_id,
630                    revision,
631                }))
632            }
633        }
634    }
635
636    pub(super) fn list<'a>(
637        db: &'a sql::ConnectionThreadSafe,
638        rid: &RepoId,
639    ) -> Result<PatchesIter<'a>, Error> {
640        let mut stmt = db.prepare(
641            "SELECT id, patch
642             FROM patches
643             WHERE repo = ?1
644             ORDER BY id
645            ",
646        )?;
647        stmt.bind((1, rid))?;
648        Ok(PatchesIter {
649            inner: stmt.into_iter(),
650        })
651    }
652
653    pub(super) fn list_by_status<'a>(
654        db: &'a sql::ConnectionThreadSafe,
655        rid: &RepoId,
656        filter: &Status,
657    ) -> Result<PatchesIter<'a>, Error> {
658        let mut stmt = db.prepare(
659            "SELECT patches.id, patch
660             FROM patches
661             WHERE repo = ?1
662             AND patch->>'$.state.status' = ?2
663             ORDER BY id
664            ",
665        )?;
666        stmt.bind((1, rid))?;
667        stmt.bind((2, sql::Value::String(filter.to_string())))?;
668        Ok(PatchesIter {
669            inner: stmt.into_iter(),
670        })
671    }
672
673    pub(super) fn counts(
674        db: &sql::ConnectionThreadSafe,
675        rid: &RepoId,
676    ) -> Result<PatchCounts, Error> {
677        let mut stmt = db.prepare(
678            "SELECT
679                 patch->'$.state' AS state,
680                 COUNT(*) AS count
681             FROM patches
682             WHERE repo = ?1
683             GROUP BY patch->'$.state.status'",
684        )?;
685        stmt.bind((1, rid))?;
686
687        stmt.into_iter()
688            .try_fold(PatchCounts::default(), |mut counts, row| {
689                let row = row?;
690                let count = row.try_read::<i64, _>("count")? as usize;
691                let status = serde_json::from_str::<State>(row.try_read::<&str, _>("state")?)?;
692                match status {
693                    State::Draft => counts.draft += count,
694                    State::Open { .. } => counts.open += count,
695                    State::Archived => counts.archived += count,
696                    State::Merged { .. } => counts.merged += count,
697                }
698                Ok(counts)
699            })
700    }
701}
702
703#[allow(clippy::unwrap_used)]
704#[cfg(test)]
705mod tests {
706    use std::collections::{BTreeMap, BTreeSet};
707    use std::num::NonZeroU8;
708    use std::str::FromStr;
709
710    use amplify::Wrapper;
711    use radicle_cob::ObjectId;
712
713    use crate::cob::cache::{Store, Update, Write};
714    use crate::cob::thread::{Comment, Thread};
715    use crate::cob::{migrate, Author, Title};
716    use crate::patch::{
717        ByRevision, MergeTarget, Patch, PatchCounts, PatchId, Revision, RevisionId, State, Status,
718    };
719    use crate::prelude::Did;
720    use crate::profile::env;
721    use crate::test::arbitrary;
722    use crate::test::storage::MockRepository;
723
724    use super::{Cache, Patches};
725
726    fn memory(store: MockRepository) -> Cache<MockRepository, Store<Write>> {
727        let cache = Store::<Write>::memory()
728            .unwrap()
729            .with_migrations(migrate::ignore)
730            .unwrap();
731        Cache { store, cache }
732    }
733
734    fn revision() -> (RevisionId, Revision) {
735        let author = arbitrary::gen::<Did>(1);
736        let description = arbitrary::gen::<String>(1);
737        let base = arbitrary::oid();
738        let oid = arbitrary::oid();
739        let timestamp = env::local_time();
740        let resolves = BTreeSet::new();
741        let id = RevisionId::from(arbitrary::oid());
742        let mut revision = Revision::new(
743            id,
744            Author { id: author },
745            description,
746            base,
747            oid,
748            timestamp.into(),
749            resolves,
750        );
751        let comment = Comment::new(
752            *author,
753            "#1 comment".to_string(),
754            None,
755            None,
756            vec![],
757            timestamp.into(),
758        );
759        let thread = Thread::new(arbitrary::oid(), comment);
760        revision.discussion = thread;
761        (id, revision)
762    }
763
764    #[test]
765    fn test_is_empty() {
766        let repo = arbitrary::gen::<MockRepository>(1);
767        let mut cache = memory(repo);
768        assert!(cache.is_empty().unwrap());
769
770        let patch = Patch::new(
771            Title::new("Patch #1").unwrap(),
772            MergeTarget::Delegates,
773            revision(),
774        );
775        let id = ObjectId::from_str("47799cbab2eca047b6520b9fce805da42b49ecab").unwrap();
776        cache.update(&cache.rid(), &id, &patch).unwrap();
777
778        let patch = Patch {
779            state: State::Archived,
780            ..Patch::new(
781                Title::new("Patch #2").unwrap(),
782                MergeTarget::Delegates,
783                revision(),
784            )
785        };
786        let id = ObjectId::from_str("ae981ded6ed2ed2cdba34c8603714782667f18a3").unwrap();
787        cache.update(&cache.rid(), &id, &patch).unwrap();
788
789        assert!(!cache.is_empty().unwrap())
790    }
791
792    #[test]
793    fn test_counts() {
794        let repo = arbitrary::gen::<MockRepository>(1);
795        let mut cache = memory(repo);
796        let n_open = arbitrary::gen::<u8>(0);
797        let n_draft = arbitrary::gen::<u8>(1);
798        let n_archived = arbitrary::gen::<u8>(1);
799        let n_merged = arbitrary::gen::<u8>(1);
800        let open_ids = (0..n_open)
801            .map(|_| PatchId::from(arbitrary::oid()))
802            .collect::<BTreeSet<PatchId>>();
803        let draft_ids = (0..n_draft)
804            .map(|_| PatchId::from(arbitrary::oid()))
805            .collect::<BTreeSet<PatchId>>();
806        let archived_ids = (0..n_archived)
807            .map(|_| PatchId::from(arbitrary::oid()))
808            .collect::<BTreeSet<PatchId>>();
809        let merged_ids = (0..n_merged)
810            .map(|_| PatchId::from(arbitrary::oid()))
811            .collect::<BTreeSet<PatchId>>();
812
813        for id in open_ids.iter() {
814            let patch = Patch::new(
815                Title::new(&id.to_string()).unwrap(),
816                MergeTarget::Delegates,
817                revision(),
818            );
819            cache
820                .update(&cache.rid(), &PatchId::from(*id), &patch)
821                .unwrap();
822        }
823
824        for id in draft_ids.iter() {
825            let patch = Patch {
826                state: State::Draft,
827                ..Patch::new(
828                    Title::new(&id.to_string()).unwrap(),
829                    MergeTarget::Delegates,
830                    revision(),
831                )
832            };
833            cache
834                .update(&cache.rid(), &PatchId::from(*id), &patch)
835                .unwrap();
836        }
837
838        for id in archived_ids.iter() {
839            let patch = Patch {
840                state: State::Archived,
841                ..Patch::new(
842                    Title::new(&id.to_string()).unwrap(),
843                    MergeTarget::Delegates,
844                    revision(),
845                )
846            };
847            cache
848                .update(&cache.rid(), &PatchId::from(*id), &patch)
849                .unwrap();
850        }
851
852        for id in merged_ids.iter() {
853            let patch = Patch {
854                state: State::Merged {
855                    revision: arbitrary::oid().into(),
856                    commit: arbitrary::oid(),
857                },
858                ..Patch::new(
859                    Title::new(&id.to_string()).unwrap(),
860                    MergeTarget::Delegates,
861                    revision(),
862                )
863            };
864            cache
865                .update(&cache.rid(), &PatchId::from(*id), &patch)
866                .unwrap();
867        }
868
869        assert_eq!(
870            cache.counts().unwrap(),
871            PatchCounts {
872                open: open_ids.len(),
873                draft: draft_ids.len(),
874                archived: archived_ids.len(),
875                merged: merged_ids.len(),
876            }
877        );
878    }
879
880    #[test]
881    fn test_get() {
882        let repo = arbitrary::gen::<MockRepository>(1);
883        let mut cache = memory(repo);
884        let ids = (0..arbitrary::gen::<u8>(1))
885            .map(|_| PatchId::from(arbitrary::oid()))
886            .collect::<BTreeSet<PatchId>>();
887        let missing = (0..arbitrary::gen::<u8>(2))
888            .filter_map(|_| {
889                let id = PatchId::from(arbitrary::oid());
890                (!ids.contains(&id)).then_some(id)
891            })
892            .collect::<BTreeSet<PatchId>>();
893        let mut patches = Vec::with_capacity(ids.len());
894
895        for id in ids.iter() {
896            let patch = Patch::new(
897                Title::new(&id.to_string()).unwrap(),
898                MergeTarget::Delegates,
899                revision(),
900            );
901            cache
902                .update(&cache.rid(), &PatchId::from(*id), &patch)
903                .unwrap();
904            patches.push((*id, patch));
905        }
906
907        for (id, patch) in patches.into_iter() {
908            assert_eq!(Some(patch), cache.get(&id).unwrap());
909        }
910
911        for id in &missing {
912            assert_eq!(cache.get(id).unwrap(), None);
913        }
914    }
915
916    #[test]
917    fn test_find_by_revision() {
918        let repo = arbitrary::gen::<MockRepository>(1);
919        let mut cache = memory(repo);
920        let patch_id = PatchId::from(arbitrary::oid());
921        let revisions = (0..arbitrary::gen::<NonZeroU8>(1).into())
922            .map(|_| revision())
923            .collect::<BTreeMap<RevisionId, Revision>>();
924        let (rev_id, rev) = revisions
925            .iter()
926            .next()
927            .expect("at least one revision should have been created");
928        let mut patch = Patch::new(
929            Title::new(&patch_id.to_string()).unwrap(),
930            MergeTarget::Delegates,
931            (*rev_id, rev.clone()),
932        );
933        let timeline = revisions.keys().copied().collect::<Vec<_>>();
934        patch
935            .timeline
936            .extend(timeline.iter().map(|id| id.into_inner()));
937        patch
938            .revisions
939            .extend(revisions.iter().map(|(id, rev)| (*id, Some(rev.clone()))));
940        cache
941            .update(&cache.rid(), &PatchId::from(*patch_id), &patch)
942            .unwrap();
943
944        for entry in timeline {
945            let rev = revisions.get(&entry).unwrap().clone();
946            assert_eq!(
947                Some(ByRevision {
948                    id: patch_id,
949                    patch: patch.clone(),
950                    revision_id: entry,
951                    revision: rev
952                }),
953                cache.find_by_revision(&entry).unwrap()
954            );
955        }
956    }
957
958    #[test]
959    fn test_list() {
960        let repo = arbitrary::gen::<MockRepository>(1);
961        let mut cache = memory(repo);
962        let ids = (0..arbitrary::gen::<u8>(1))
963            .map(|_| PatchId::from(arbitrary::oid()))
964            .collect::<BTreeSet<PatchId>>();
965        let mut patches = Vec::with_capacity(ids.len());
966
967        for id in ids.iter() {
968            let patch = Patch::new(
969                Title::new(&id.to_string()).unwrap(),
970                MergeTarget::Delegates,
971                revision(),
972            );
973            cache
974                .update(&cache.rid(), &PatchId::from(*id), &patch)
975                .unwrap();
976            patches.push((*id, patch));
977        }
978
979        let mut list = cache
980            .list()
981            .unwrap()
982            .collect::<Result<Vec<_>, _>>()
983            .unwrap();
984        list.sort_by_key(|(id, _)| *id);
985        patches.sort_by_key(|(id, _)| *id);
986        assert_eq!(patches, list);
987    }
988
989    #[test]
990    fn test_list_by_status() {
991        let repo = arbitrary::gen::<MockRepository>(1);
992        let mut cache = memory(repo);
993        let ids = (0..arbitrary::gen::<u8>(1))
994            .map(|_| PatchId::from(arbitrary::oid()))
995            .collect::<BTreeSet<PatchId>>();
996        let mut patches = Vec::with_capacity(ids.len());
997
998        for id in ids.iter() {
999            let patch = Patch::new(
1000                Title::new(&id.to_string()).unwrap(),
1001                MergeTarget::Delegates,
1002                revision(),
1003            );
1004            cache
1005                .update(&cache.rid(), &PatchId::from(*id), &patch)
1006                .unwrap();
1007            patches.push((*id, patch));
1008        }
1009
1010        let mut list = cache
1011            .list_by_status(&Status::Open)
1012            .unwrap()
1013            .collect::<Result<Vec<_>, _>>()
1014            .unwrap();
1015        list.sort_by_key(|(id, _)| *id);
1016        patches.sort_by_key(|(id, _)| *id);
1017        assert_eq!(patches, list);
1018    }
1019
1020    #[test]
1021    fn test_remove() {
1022        let repo = arbitrary::gen::<MockRepository>(1);
1023        let mut cache = memory(repo);
1024        let ids = (0..arbitrary::gen::<u8>(1))
1025            .map(|_| PatchId::from(arbitrary::oid()))
1026            .collect::<BTreeSet<PatchId>>();
1027
1028        for id in ids.iter() {
1029            let patch = Patch::new(
1030                Title::new(&id.to_string()).unwrap(),
1031                MergeTarget::Delegates,
1032                revision(),
1033            );
1034            cache
1035                .update(&cache.rid(), &PatchId::from(*id), &patch)
1036                .unwrap();
1037            assert_eq!(Some(patch), cache.get(id).unwrap());
1038            super::Remove::remove(&mut cache, id).unwrap();
1039            assert_eq!(None, cache.get(id).unwrap());
1040        }
1041    }
1042}