Skip to main content

interprex_test/
state.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    sync::Arc,
4};
5
6use bytes::Bytes;
7use interprex::{
8    AssetId, ChangeRequest, ChangeRequestNumber, CheckOutcome, CheckRun, DispatchInputs, Issue,
9    IssueNumber, Label, ProviderAppId, ProviderError, Release, Repository, RepositoryFacts,
10    RepositorySettings, ReviewActorId, ReviewId, ReviewPublicationKey, ReviewRequestTarget,
11    ReviewSubmission, ReviewTarget, ReviewerApplication, Ruleset, RunId, WorkflowRun,
12};
13use tokio::sync::RwLock;
14
15#[derive(Clone, Debug, Default)]
16pub struct FakeProvider {
17    pub(crate) state: Arc<RwLock<State>>,
18}
19
20#[derive(Debug, Default)]
21pub(crate) struct State {
22    pub(crate) repositories: BTreeMap<Repository, (RepositoryFacts, RepositorySettings)>,
23    pub(crate) rulesets: BTreeMap<Repository, Vec<Ruleset>>,
24    pub(crate) secret_names: BTreeMap<Repository, Vec<String>>,
25    pub(crate) issues: BTreeMap<(Repository, IssueNumber), Issue>,
26    pub(crate) labels: BTreeMap<Repository, Vec<Label>>,
27    pub(crate) change_requests: BTreeMap<(Repository, ChangeRequestNumber), ChangeRequest>,
28    pub(crate) review_target_observations: Vec<(Repository, ReviewRequestTarget, ReviewTarget)>,
29    pub(crate) reviewer_applications: BTreeMap<(Repository, String), ReviewerApplication>,
30    pub(crate) review_publications: BTreeMap<FakeReviewPublicationKey, FakeReviewPublication>,
31    pub(crate) check_runs: BTreeMap<(Repository, String), Vec<CheckRun>>,
32    pub(crate) published_checks: Vec<(Repository, String, CheckOutcome)>,
33    pub(crate) dispatches: Vec<(Repository, String, String, DispatchInputs)>,
34    pub(crate) runs: BTreeMap<(Repository, RunId), WorkflowRun>,
35    pub(crate) cancelled_runs: Vec<(Repository, RunId)>,
36    pub(crate) releases: BTreeMap<(Repository, String), Release>,
37    pub(crate) assets: BTreeMap<(Repository, AssetId), Vec<Bytes>>,
38    pub(crate) next_release_id: u64,
39    pub(crate) next_asset_id: u64,
40}
41
42pub(crate) type FakeReviewPublicationKey = (
43    Repository,
44    ChangeRequestNumber,
45    ProviderAppId,
46    ReviewActorId,
47    ReviewPublicationKey,
48);
49
50#[derive(Clone, Debug)]
51pub(crate) struct FakeReviewPublication {
52    pub(crate) submission: ReviewSubmission,
53    pub(crate) review_id: ReviewId,
54}
55
56impl FakeProvider {
57    #[must_use]
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    pub async fn seed_repository(&self, facts: RepositoryFacts, settings: RepositorySettings) {
63        self.state
64            .write()
65            .await
66            .repositories
67            .insert(facts.repository.clone(), (facts, settings));
68    }
69
70    pub async fn seed_issue(&self, repository: Repository, issue: Issue) {
71        self.state
72            .write()
73            .await
74            .issues
75            .insert((repository, issue.number), issue);
76    }
77
78    /// Seeds one change request into the repository it targets.
79    ///
80    /// The head it proposes is `change_request.head`, which names the
81    /// repository holding that branch and so can be a fork of `repository`.
82    /// Comment collections retain their declared order; the fake neither sorts
83    /// them nor derives order from opaque comment identifiers.
84    pub async fn seed_change_request(&self, repository: Repository, change_request: ChangeRequest) {
85        self.state
86            .write()
87            .await
88            .change_requests
89            .insert((repository, change_request.number), change_request);
90    }
91
92    /// Seeds the provider observation returned for one review-request target.
93    ///
94    /// The target category is part of the lookup key while `observed` supplies
95    /// the actual actor or team category. Keeping them separate lets tests
96    /// model a request target that resolves to the wrong kind without the fake
97    /// inferring identity facts from the requested enum variant.
98    pub async fn seed_review_request_target(
99        &self,
100        repository: Repository,
101        target: ReviewRequestTarget,
102        observed: ReviewTarget,
103    ) {
104        let mut state = self.state.write().await;
105        state
106            .review_target_observations
107            .retain(|(seeded_repository, seeded_target, _)| {
108                seeded_repository != &repository || seeded_target != &target
109            });
110        state
111            .review_target_observations
112            .push((repository, target, observed));
113    }
114
115    /// Maps one lookup slug to the application and bot identity returned for a
116    /// repository.
117    ///
118    /// The fake does not derive this mapping from the application's canonical
119    /// slug or from a seeded review target.
120    pub async fn seed_reviewer_application(
121        &self,
122        repository: Repository,
123        slug: String,
124        application: ReviewerApplication,
125    ) {
126        self.state
127            .write()
128            .await
129            .reviewer_applications
130            .insert((repository, slug), application);
131    }
132
133    /// Seeds observed checks, each on the commit it names, replacing whatever
134    /// was already seeded on the commits this call names.
135    ///
136    /// The commit comes from every run's own `head_sha`, so no seeded
137    /// observation can place a run on a commit it does not name.
138    pub async fn seed_check_runs(&self, repository: Repository, runs: Vec<CheckRun>) {
139        let mut state = self.state.write().await;
140        let mut replaced = BTreeSet::new();
141        for run in runs {
142            let key = (repository.clone(), run.head_sha.clone());
143            if replaced.insert(key.clone()) {
144                state.check_runs.insert(key.clone(), Vec::new());
145            }
146            state.check_runs.entry(key).or_default().push(run);
147        }
148    }
149
150    pub async fn seed_run(&self, repository: Repository, run: WorkflowRun) {
151        self.state
152            .write()
153            .await
154            .runs
155            .insert((repository, run.id), run);
156    }
157
158    pub async fn seed_release(&self, repository: Repository, release: Release) {
159        self.state
160            .write()
161            .await
162            .releases
163            .insert((repository, release.tag.clone()), release);
164    }
165
166    pub async fn published_checks(&self) -> Vec<(Repository, String, CheckOutcome)> {
167        self.state.read().await.published_checks.clone()
168    }
169
170    pub async fn dispatches(&self) -> Vec<(Repository, String, String, DispatchInputs)> {
171        self.state.read().await.dispatches.clone()
172    }
173}
174
175pub(crate) fn missing(entity: impl Into<String>) -> ProviderError {
176    ProviderError::NotFound {
177        entity: entity.into(),
178    }
179}