gitlab_runner_mock/
job.rs

1use serde::Deserialize;
2use serde::Serialize;
3use std::collections::HashMap;
4use std::fmt::Display;
5use std::sync::Arc;
6use std::sync::Mutex;
7use thiserror::Error;
8
9use crate::variables::default_job_variables;
10
11#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum MockJobState {
14    Pending,
15    Running,
16    Success,
17    Failed,
18    Cancelled,
19}
20
21impl MockJobState {
22    pub fn finished(self) -> bool {
23        self == Self::Success || self == Self::Failed
24    }
25}
26
27impl Display for MockJobState {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        let d = match *self {
30            MockJobState::Pending => "pending",
31            MockJobState::Running => "running",
32            MockJobState::Success => "success",
33            MockJobState::Failed => "failed",
34            // The spelling mismatch of "cancelled" vs "canceled" is
35            // intentional: this crate, as well as tokio_util, already use
36            // "cancelled", so using it here keeps the spelling consistent, even
37            // if it's not *identical* to the exact GitLab job status.
38            MockJobState::Cancelled => "canceled",
39        };
40        write!(f, "{d}")
41    }
42}
43
44#[derive(Debug, Error)]
45pub enum LogError {
46    #[error("Incorrect range start")]
47    IncorrectStart,
48    #[error("Incorrect range end")]
49    IncorrectEnd,
50}
51
52#[derive(Clone, Debug)]
53pub struct MockUploadedJobArtifact {
54    pub filename: Option<String>,
55    pub data: Arc<Vec<u8>>,
56    pub artifact_format: Option<String>,
57    pub artifact_type: Option<String>,
58}
59
60#[derive(Debug)]
61pub(crate) struct MockJobInner {
62    state: MockJobState,
63    state_updates: u32,
64    uploaded_artifacts: Vec<MockUploadedJobArtifact>,
65    log: Vec<Vec<u8>>,
66    log_patches: u32,
67}
68
69#[derive(Clone, Serialize, Debug)]
70pub struct MockJobVariable {
71    pub key: String,
72    pub value: String,
73    pub public: bool,
74    pub masked: bool,
75}
76
77#[derive(Copy, Clone, Serialize, Debug, Eq, PartialEq)]
78#[serde(rename_all = "snake_case")]
79pub enum MockJobStepWhen {
80    Always,
81    OnFailure,
82    OnSuccess,
83}
84
85#[derive(Copy, Clone, Serialize, Debug, Eq, PartialEq)]
86#[serde(rename_all = "snake_case")]
87pub enum MockJobStepName {
88    Script,
89    AfterScript,
90}
91
92#[derive(Clone, Serialize, Debug)]
93pub struct MockJobStep {
94    name: MockJobStepName,
95    script: Vec<String>,
96    timeout: u64,
97    when: MockJobStepWhen,
98    allow_failure: bool,
99}
100
101#[derive(Copy, Clone, Serialize, Debug, Eq, PartialEq)]
102#[serde(rename_all = "snake_case")]
103pub enum MockJobArtifactWhen {
104    Always,
105    OnFailure,
106    OnSuccess,
107}
108
109#[derive(Clone, Serialize, Debug)]
110pub struct MockJobArtifact {
111    pub name: Option<String>,
112    pub untracked: bool,
113    pub paths: Vec<String>,
114    pub when: Option<MockJobArtifactWhen>,
115    pub artifact_type: String,
116    pub artifact_format: Option<String>,
117    pub expire_in: Option<String>,
118}
119
120#[derive(Clone, Debug)]
121pub struct MockJob {
122    name: String,
123    id: u64,
124    token: String,
125    variables: Vec<MockJobVariable>,
126    steps: Vec<MockJobStep>,
127    dependencies: Vec<MockJob>,
128    artifacts: Vec<MockJobArtifact>,
129    inner: Arc<Mutex<MockJobInner>>,
130}
131
132impl MockJob {
133    pub(crate) fn new(name: String, id: u64) -> Self {
134        let mut builder = MockJobBuilder::new(name, id);
135        builder.add_step(
136            MockJobStepName::Script,
137            vec!["dummy".to_string()],
138            3600,
139            MockJobStepWhen::OnSuccess,
140            false,
141        );
142
143        builder.build()
144    }
145
146    pub(crate) fn new_completed(name: String, id: u64, artifact: Vec<u8>) -> Self {
147        let artifacts = if artifact.is_empty() {
148            Vec::new()
149        } else {
150            vec![MockUploadedJobArtifact {
151                filename: Some("default.zip".to_string()),
152                data: Arc::new(artifact),
153                artifact_format: None,
154                artifact_type: None,
155            }]
156        };
157
158        Self {
159            name,
160            id,
161            token: format!("job-token-{id}"),
162            variables: Vec::new(),
163            steps: Vec::new(),
164            dependencies: Vec::new(),
165            artifacts: Vec::new(),
166            inner: Arc::new(Mutex::new(MockJobInner {
167                state: MockJobState::Success,
168                state_updates: 2,
169                uploaded_artifacts: artifacts,
170                log: Vec::new(),
171                log_patches: 0,
172            })),
173        }
174    }
175
176    pub fn name(&self) -> &str {
177        &self.name
178    }
179
180    pub fn id(&self) -> u64 {
181        self.id
182    }
183
184    pub fn token(&self) -> &str {
185        &self.token
186    }
187
188    pub fn dependencies(&self) -> &[MockJob] {
189        &self.dependencies
190    }
191
192    pub fn artifacts(&self) -> &[MockJobArtifact] {
193        &self.artifacts
194    }
195
196    pub fn variables(&self) -> &[MockJobVariable] {
197        &self.variables
198    }
199
200    pub fn steps(&self) -> &[MockJobStep] {
201        &self.steps
202    }
203
204    pub fn state(&self) -> MockJobState {
205        let inner = self.inner.lock().unwrap();
206        inner.state
207    }
208
209    pub fn state_updates(&self) -> u32 {
210        let inner = self.inner.lock().unwrap();
211        inner.state_updates
212    }
213
214    pub fn finished(&self) -> bool {
215        let inner = self.inner.lock().unwrap();
216        inner.state.finished()
217    }
218
219    pub fn log_last(&self) -> Option<Vec<u8>> {
220        let inner = self.inner.lock().unwrap();
221        inner.log.last().cloned()
222    }
223
224    pub fn log(&self) -> Vec<u8> {
225        let inner = self.inner.lock().unwrap();
226        inner.log.concat()
227    }
228
229    pub fn log_patches(&self) -> u32 {
230        let inner = self.inner.lock().unwrap();
231        inner.log_patches
232    }
233
234    pub fn uploaded_artifacts(&self) -> impl Iterator<Item = MockUploadedJobArtifact> {
235        let inner = self.inner.lock().unwrap();
236        inner.uploaded_artifacts.clone().into_iter()
237    }
238
239    pub fn cancel(&self) {
240        let mut inner = self.inner.lock().unwrap();
241        assert!(!inner.state.finished(), "Job is already finished");
242        inner.state_updates += 1;
243        inner.state = MockJobState::Cancelled;
244    }
245
246    pub(crate) fn update_state(&self, state: MockJobState) {
247        let mut inner = self.inner.lock().unwrap();
248        inner.state_updates += 1;
249        inner.state = state;
250    }
251
252    pub(crate) fn append_log(
253        &self,
254        data: Vec<u8>,
255        start: usize,
256        end: usize,
257    ) -> Result<(), LogError> {
258        let mut inner = self.inner.lock().unwrap();
259
260        let log_len = inner.log.iter().fold(0, |acc, l| acc + l.len());
261        if log_len != start {
262            return Err(LogError::IncorrectStart);
263        }
264
265        if start + data.len() - 1 != end {
266            return Err(LogError::IncorrectEnd);
267        }
268
269        inner.log.push(data);
270        inner.log_patches += 1;
271        Ok(())
272    }
273
274    pub(crate) fn add_artifact(
275        &self,
276        filename: Option<String>,
277        data: Vec<u8>,
278        artifact_type: Option<&str>,
279        artifact_format: Option<&str>,
280    ) {
281        let mut inner = self.inner.lock().unwrap();
282        inner.uploaded_artifacts.push(MockUploadedJobArtifact {
283            filename,
284            data: Arc::new(data),
285            artifact_format: artifact_format.map(str::to_string),
286            artifact_type: artifact_type.map(str::to_string),
287        });
288    }
289}
290
291#[derive(Debug, Default)]
292pub struct MockJobBuilder {
293    name: String,
294    id: u64,
295    variables: HashMap<String, MockJobVariable>,
296    steps: Vec<MockJobStep>,
297    dependencies: Vec<MockJob>,
298    artifacts: Vec<MockJobArtifact>,
299}
300
301impl MockJobBuilder {
302    pub(crate) fn new(name: String, id: u64) -> Self {
303        Self {
304            name,
305            id,
306            variables: default_job_variables(id)
307                .into_iter()
308                .map(|v| (v.key.clone(), v))
309                .collect(),
310            ..Default::default()
311        }
312    }
313
314    pub fn add_variable(&mut self, key: String, value: String, public: bool, masked: bool) {
315        self.variables.insert(
316            key.clone(),
317            MockJobVariable {
318                key,
319                value,
320                public,
321                masked,
322            },
323        );
324    }
325
326    pub fn add_step(
327        &mut self,
328        name: MockJobStepName,
329        script: Vec<String>,
330        timeout: u64,
331        when: MockJobStepWhen,
332        allow_failure: bool,
333    ) {
334        if self.steps.iter().any(|s| s.name == name) {
335            panic!("Step already exists!");
336        }
337
338        let step = MockJobStep {
339            name,
340            script,
341            timeout,
342            when,
343            allow_failure,
344        };
345
346        self.steps.push(step);
347    }
348
349    #[allow(clippy::too_many_arguments)]
350    pub fn add_artifact(
351        &mut self,
352        name: Option<String>,
353        untracked: bool,
354        paths: Vec<String>,
355        when: Option<MockJobArtifactWhen>,
356        artifact_type: String,
357        artifact_format: Option<String>,
358        expire_in: Option<String>,
359    ) {
360        self.artifacts.push(MockJobArtifact {
361            name,
362            untracked,
363            paths,
364            when,
365            artifact_type,
366            artifact_format,
367            expire_in,
368        });
369    }
370
371    pub fn add_artifact_paths(&mut self, paths: Vec<String>) {
372        self.add_artifact(
373            None,
374            false,
375            paths,
376            None,
377            "archive".to_string(),
378            Some("zip".to_string()),
379            None,
380        );
381    }
382
383    pub fn dependency(&mut self, dependency: MockJob) {
384        self.dependencies.push(dependency);
385    }
386
387    pub fn build(self) -> MockJob {
388        assert!(!self.steps.is_empty(), "Should have at least one step");
389        let inner = MockJobInner {
390            state: MockJobState::Pending,
391            state_updates: 0,
392            log: Vec::new(),
393            log_patches: 0,
394            uploaded_artifacts: Vec::new(),
395        };
396
397        let inner = Arc::new(Mutex::new(inner));
398        MockJob {
399            name: self.name,
400            id: self.id,
401            token: format!("job-token-{}", self.id),
402            steps: self.steps,
403            variables: self.variables.into_values().collect(),
404            dependencies: self.dependencies,
405            artifacts: self.artifacts,
406            inner,
407        }
408    }
409}