gitlab_runner_mock/
lib.rs1#![recursion_limit = "256"]
3
4use std::sync::Arc;
5use std::sync::Mutex;
6use url::Url;
7use wiremock::matchers::{body_json_schema, method, path, path_regex};
8use wiremock::{Mock, MockServer};
9
10mod job;
11mod variables;
12pub use job::{
13 MockJob, MockJobArtifactWhen, MockJobBuilder, MockJobState, MockJobStepName, MockJobStepWhen,
14};
15
16mod api;
17use api::JobArtifactsDownloader;
18use api::JobArtifactsUploader;
19use api::JobTraceResponder;
20use api::JobUpdateResponder;
21use api::{JobRequest, JobRequestResponder};
22
23#[derive(Default)]
24struct JobData {
25 last_id: u64,
26 jobs: Vec<MockJob>,
27}
28
29#[derive(Default, Clone)]
30struct ExpectedMetadata {
31 version: Option<String>,
32 revision: Option<String>,
33 platform: Option<String>,
34 architecture: Option<String>,
35}
36
37#[derive(Default, Clone)]
38struct ArtifactDownloadsCounter {
39 count: u32,
40 failure_count: u32,
41}
42
43struct Inner {
44 server: MockServer,
45 runner_token: String,
46 jobs: Mutex<JobData>,
47 update_interval: Mutex<u32>,
48 expected_metadata: Mutex<ExpectedMetadata>,
49 artifact_downloads_counter: Mutex<ArtifactDownloadsCounter>,
50}
51
52#[derive(Clone)]
53pub struct GitlabRunnerMock {
54 inner: Arc<Inner>,
55}
56
57impl GitlabRunnerMock {
58 pub async fn start() -> Self {
59 let m = MockServer::start().await;
60 let jobs = JobData {
61 last_id: 265,
62 ..Default::default()
63 };
64 let inner = Inner {
65 server: m,
66 runner_token: "fakerunnertoken".to_string(),
67 jobs: Mutex::new(jobs),
68 update_interval: Mutex::new(3),
69 expected_metadata: Mutex::new(ExpectedMetadata::default()),
70 artifact_downloads_counter: Mutex::new(ArtifactDownloadsCounter::default()),
71 };
72 let server = Self {
73 inner: Arc::new(inner),
74 };
75
76 Mock::given(method("POST"))
77 .and(path("api/v4/jobs/request"))
78 .and(body_json_schema::<JobRequest>)
79 .respond_with(JobRequestResponder::new(server.clone()))
80 .mount(&server.inner.server)
81 .await;
82
83 Mock::given(method("PUT"))
84 .and(path_regex(r"^/api/v4/jobs/\d+$"))
85 .respond_with(JobUpdateResponder::new(server.clone()))
87 .mount(&server.inner.server)
88 .await;
89
90 Mock::given(method("PATCH"))
91 .and(path_regex(r"^/api/v4/jobs/\d+/trace"))
92 .respond_with(JobTraceResponder::new(server.clone()))
93 .mount(&server.inner.server)
94 .await;
95
96 Mock::given(method("POST"))
97 .and(path_regex(r"^/api/v4/jobs/\d+/artifacts"))
98 .respond_with(JobArtifactsUploader::new(server.clone()))
99 .mount(&server.inner.server)
100 .await;
101
102 Mock::given(method("GET"))
103 .and(path_regex(r"^/api/v4/jobs/\d+/artifacts"))
104 .respond_with(JobArtifactsDownloader::new(server.clone()))
105 .mount(&server.inner.server)
106 .await;
107
108 server
109 }
110
111 pub fn uri(&self) -> Url {
112 self.inner.server.uri().parse().expect("uri is not a url")
113 }
114
115 pub fn runner_token(&self) -> &str {
116 &self.inner.runner_token
117 }
118
119 pub fn add_dummy_job(&self, name: String) -> MockJob {
120 let mut jobs = self.inner.jobs.lock().unwrap();
121 jobs.last_id += 1;
122
123 let job = MockJob::new(name, jobs.last_id);
124 jobs.jobs.push(job.clone());
125 job
126 }
127
128 pub fn add_completed_job(&self, name: String, artifact: Vec<u8>) -> MockJob {
129 let mut jobs = self.inner.jobs.lock().unwrap();
130 jobs.last_id += 1;
131
132 let job = MockJob::new_completed(name, jobs.last_id, artifact);
133
134 jobs.jobs.push(job.clone());
135
136 job
137 }
138
139 pub fn job_builder(&self, name: String) -> MockJobBuilder {
140 let mut jobs = self.inner.jobs.lock().unwrap();
141 jobs.last_id += 1;
142
143 MockJobBuilder::new(name, jobs.last_id)
144 }
145
146 pub fn enqueue_job(&self, job: MockJob) {
147 let mut jobs = self.inner.jobs.lock().unwrap();
148 if jobs.jobs.iter().any(|j| j.id() == job.id()) {
149 panic!("Trying to requeue already queued job");
150 }
151
152 jobs.jobs.push(job);
153 }
154
155 fn grab_pending_job(&self) -> Option<MockJob> {
156 let jobs = self.inner.jobs.lock().unwrap();
157 for job in jobs.jobs.iter() {
158 if job.state() == MockJobState::Pending
159 && job.dependencies().iter().all(|d| d.state().finished())
160 {
161 return Some(job.clone());
162 }
163 }
164 None
165 }
166
167 pub fn get_job(&self, id: u64) -> Option<MockJob> {
168 let jobs = self.inner.jobs.lock().unwrap();
169 for job in jobs.jobs.iter() {
170 if job.id() == id {
171 return Some(job.clone());
172 }
173 }
174 None
175 }
176
177 pub async fn n_requests(&self) -> usize {
178 let requests = self.inner.server.received_requests().await.unwrap();
179 requests
180 .iter()
181 .filter(|r| r.url.path() == "/api/v4/jobs/request")
182 .count()
183 }
184
185 pub fn update_interval(&self) -> u32 {
186 *self.inner.update_interval.lock().unwrap()
187 }
188
189 pub fn set_update_interval(&self, interval: u32) {
190 *self.inner.update_interval.lock().unwrap() = interval;
191 }
192
193 pub(crate) fn expected_metadata(&self) -> ExpectedMetadata {
194 self.inner.expected_metadata.lock().unwrap().clone()
195 }
196
197 pub fn set_expected_metadata_version<S: Into<String>>(&self, version: S) {
198 self.inner.expected_metadata.lock().unwrap().version = Some(version.into());
199 }
200
201 pub fn set_expected_metadata_revision<S: Into<String>>(&self, revision: S) {
202 self.inner.expected_metadata.lock().unwrap().revision = Some(revision.into());
203 }
204
205 pub fn set_expected_metadata_platform<S: Into<String>>(&self, platform: S) {
206 self.inner.expected_metadata.lock().unwrap().platform = Some(platform.into());
207 }
208
209 pub fn set_expected_metadata_architecture<S: Into<String>>(&self, architecture: S) {
210 self.inner.expected_metadata.lock().unwrap().architecture = Some(architecture.into());
211 }
212
213 pub fn set_inject_artifact_download_failures(&self, failure_count: u32) {
214 self.inner
215 .artifact_downloads_counter
216 .lock()
217 .unwrap()
218 .failure_count = failure_count;
219 }
220}