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    AppliedSourceRequirements, AssetId, BranchUpdateObservation, ChangeRequest,
9    ChangeRequestNumber, CheckOutcome, CheckRun, CommitRange, DispatchInputs, Issue, IssueNumber,
10    Label, ProviderAppId, ProviderError, Release, Repository, RepositoryFacts, RepositorySettings,
11    ReviewActorId, ReviewId, ReviewPublicationKey, ReviewRequestTarget, ReviewSubmission,
12    ReviewTarget, ReviewerApplication, RunId, WorkflowRun,
13};
14use tokio::sync::RwLock;
15
16#[derive(Clone, Debug, Default)]
17pub struct FakeProvider {
18    pub(crate) state: Arc<RwLock<State>>,
19}
20
21#[derive(Debug, Default)]
22pub(crate) struct State {
23    pub(crate) repositories: BTreeMap<Repository, (RepositoryFacts, RepositorySettings)>,
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) branch_updates: BTreeMap<(Repository, ChangeRequestNumber), BranchUpdateObservation>,
29    pub(crate) accepted_branch_updates: Vec<(Repository, ChangeRequestNumber, String)>,
30    pub(crate) applied_requirements:
31        BTreeMap<FakeAppliedRequirementsKey, AppliedSourceRequirements>,
32    pub(crate) applied_requirement_errors: BTreeMap<FakeAppliedRequirementsKey, ProviderError>,
33    pub(crate) review_target_observations: Vec<(Repository, ReviewRequestTarget, ReviewTarget)>,
34    pub(crate) reviewer_applications: BTreeMap<(Repository, String), ReviewerApplication>,
35    pub(crate) review_publications: BTreeMap<FakeReviewPublicationKey, FakeReviewPublication>,
36    pub(crate) check_runs: BTreeMap<(Repository, String), Vec<CheckRun>>,
37    pub(crate) published_checks: Vec<(Repository, String, CheckOutcome)>,
38    pub(crate) dispatches: Vec<(Repository, String, String, DispatchInputs)>,
39    pub(crate) runs: BTreeMap<(Repository, RunId), WorkflowRun>,
40    pub(crate) cancelled_runs: Vec<(Repository, RunId)>,
41    pub(crate) releases: BTreeMap<(Repository, String), Release>,
42    pub(crate) assets: BTreeMap<(Repository, AssetId), Vec<Bytes>>,
43    pub(crate) next_release_id: u64,
44    pub(crate) next_asset_id: u64,
45}
46
47pub(crate) type FakeReviewPublicationKey = (
48    Repository,
49    ChangeRequestNumber,
50    ProviderAppId,
51    ReviewActorId,
52    ReviewPublicationKey,
53);
54
55pub(crate) type FakeAppliedRequirementsKey = (Repository, String, String, String);
56
57#[derive(Clone, Debug)]
58pub(crate) struct FakeReviewPublication {
59    pub(crate) submission: ReviewSubmission,
60    pub(crate) review_id: ReviewId,
61}
62
63impl FakeProvider {
64    #[must_use]
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    pub async fn seed_repository(&self, facts: RepositoryFacts, settings: RepositorySettings) {
70        self.state
71            .write()
72            .await
73            .repositories
74            .insert(facts.repository.clone(), (facts, settings));
75    }
76
77    pub async fn seed_issue(&self, repository: Repository, issue: Issue) {
78        self.state
79            .write()
80            .await
81            .issues
82            .insert((repository, issue.number), issue);
83    }
84
85    /// Seeds one change request into the repository it targets.
86    ///
87    /// The head it proposes is `change_request.head`, which names the
88    /// repository holding that branch and so can be a fork of `repository`.
89    /// Comment collections retain their declared order; the fake neither sorts
90    /// them nor derives order from opaque comment identifiers.
91    pub async fn seed_change_request(&self, repository: Repository, change_request: ChangeRequest) {
92        self.state
93            .write()
94            .await
95            .change_requests
96            .insert((repository, change_request.number), change_request);
97    }
98
99    /// Seeds branch-update facts for one change request.
100    ///
101    /// The fake returns this observation unchanged. Accepted updates are
102    /// recorded separately; tests explicitly seed a later observation instead
103    /// of relying on the fake to invent a provider revision.
104    pub async fn seed_branch_update(
105        &self,
106        repository: Repository,
107        number: ChangeRequestNumber,
108        observation: BranchUpdateObservation,
109    ) {
110        self.state
111            .write()
112            .await
113            .branch_updates
114            .insert((repository, number), observation);
115    }
116
117    /// Seeds one exact applied-requirements observation.
118    ///
119    /// Repository, target branch, base revision, and head revision are all
120    /// part of the lookup key. A test must seed every snapshot it expects the
121    /// fake to answer; the fake never substitutes a neighboring revision.
122    pub async fn seed_applied_requirements(&self, observation: AppliedSourceRequirements) {
123        let range = observation.commit_range();
124        let key = (
125            observation.repository().clone(),
126            observation.target_branch().to_owned(),
127            range.base_sha.clone(),
128            range.head_sha.clone(),
129        );
130        let mut state = self.state.write().await;
131        state.applied_requirement_errors.remove(&key);
132        state.applied_requirements.insert(key, observation);
133    }
134
135    /// Seeds the provider error returned for one exact applied-requirements
136    /// request, replacing an observation for the same snapshot.
137    pub async fn seed_applied_requirements_error(
138        &self,
139        repository: Repository,
140        target_branch: impl Into<String>,
141        commit_range: CommitRange,
142        error: ProviderError,
143    ) {
144        let key = (
145            repository,
146            target_branch.into(),
147            commit_range.base_sha,
148            commit_range.head_sha,
149        );
150        let mut state = self.state.write().await;
151        state.applied_requirements.remove(&key);
152        state.applied_requirement_errors.insert(key, error);
153    }
154
155    /// Seeds the provider observation returned for one review-request target.
156    ///
157    /// The target category is part of the lookup key while `observed` supplies
158    /// the actual actor or team category. Keeping them separate lets tests
159    /// model a request target that resolves to the wrong kind without the fake
160    /// inferring identity facts from the requested enum variant.
161    pub async fn seed_review_request_target(
162        &self,
163        repository: Repository,
164        target: ReviewRequestTarget,
165        observed: ReviewTarget,
166    ) {
167        let mut state = self.state.write().await;
168        state
169            .review_target_observations
170            .retain(|(seeded_repository, seeded_target, _)| {
171                seeded_repository != &repository || seeded_target != &target
172            });
173        state
174            .review_target_observations
175            .push((repository, target, observed));
176    }
177
178    /// Maps one lookup slug to the application and bot identity returned for a
179    /// repository.
180    ///
181    /// The fake does not derive this mapping from the application's canonical
182    /// slug or from a seeded review target.
183    pub async fn seed_reviewer_application(
184        &self,
185        repository: Repository,
186        slug: String,
187        application: ReviewerApplication,
188    ) {
189        self.state
190            .write()
191            .await
192            .reviewer_applications
193            .insert((repository, slug), application);
194    }
195
196    /// Seeds observed checks, each on the commit it names, replacing whatever
197    /// was already seeded on the commits this call names.
198    ///
199    /// The commit comes from every run's own `head_sha`, so no seeded
200    /// observation can place a run on a commit it does not name.
201    pub async fn seed_check_runs(&self, repository: Repository, runs: Vec<CheckRun>) {
202        let mut state = self.state.write().await;
203        let mut replaced = BTreeSet::new();
204        for run in runs {
205            let key = (repository.clone(), run.head_sha.clone());
206            if replaced.insert(key.clone()) {
207                state.check_runs.insert(key.clone(), Vec::new());
208            }
209            state.check_runs.entry(key).or_default().push(run);
210        }
211    }
212
213    pub async fn seed_run(&self, repository: Repository, run: WorkflowRun) {
214        self.state
215            .write()
216            .await
217            .runs
218            .insert((repository, run.id), run);
219    }
220
221    pub async fn seed_release(&self, repository: Repository, release: Release) {
222        self.state
223            .write()
224            .await
225            .releases
226            .insert((repository, release.tag.clone()), release);
227    }
228
229    pub async fn published_checks(&self) -> Vec<(Repository, String, CheckOutcome)> {
230        self.state.read().await.published_checks.clone()
231    }
232
233    /// Returns accepted exact-head branch-update requests in call order.
234    pub async fn accepted_branch_updates(&self) -> Vec<(Repository, ChangeRequestNumber, String)> {
235        self.state.read().await.accepted_branch_updates.clone()
236    }
237
238    pub async fn dispatches(&self) -> Vec<(Repository, String, String, DispatchInputs)> {
239        self.state.read().await.dispatches.clone()
240    }
241}
242
243pub(crate) fn missing(entity: impl Into<String>) -> ProviderError {
244    ProviderError::NotFound {
245        entity: entity.into(),
246    }
247}