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
22pub trait Patches {
24 type Error: std::error::Error + Send + Sync + 'static;
25
26 type Iter<'a>: Iterator<Item = Result<(PatchId, Patch), Self::Error>> + 'a
28 where
29 Self: 'a;
30
31 fn get(&self, id: &PatchId) -> Result<Option<Patch>, Self::Error>;
34
35 fn find_by_revision(&self, id: &RevisionId) -> Result<Option<ByRevision>, Self::Error>;
38
39 fn list(&self) -> Result<Self::Iter<'_>, Self::Error>;
41
42 fn list_by_status(&self, status: &Status) -> Result<Self::Iter<'_>, Self::Error>;
48
49 fn counts(&self) -> Result<PatchCounts, Self::Error>;
51
52 fn opened(&self) -> Result<Self::Iter<'_>, Self::Error> {
54 self.list_by_status(&Status::Open)
55 }
56
57 fn archived(&self) -> Result<Self::Iter<'_>, Self::Error> {
59 self.list_by_status(&Status::Archived)
60 }
61
62 fn drafted(&self) -> Result<Self::Iter<'_>, Self::Error> {
64 self.list_by_status(&Status::Draft)
65 }
66
67 fn merged(&self) -> Result<Self::Iter<'_>, Self::Error> {
69 self.list_by_status(&Status::Merged)
70 }
71
72 fn is_empty(&self) -> Result<bool, Self::Error> {
74 Ok(self.counts()?.total() == 0)
75 }
76}
77
78pub trait PatchesMut: Patches + Update<Patch> + Remove<Patch> {}
81
82impl<T> PatchesMut for T where T: Patches + Update<Patch> + Remove<Patch> {}
83
84pub 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 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 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 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 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 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 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 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 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 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
420pub 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
569mod 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}