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 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}