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