Skip to content

Commit 60db5ce

Browse files
authored
Merge pull request #2145 from input-output-hk/jpraynaud/2123-retries-e2e-test
Feat: support retries in e2e tests in CI
2 parents 0ebf6bb + fcb728e commit 60db5ce

File tree

7 files changed

+202
-60
lines changed

7 files changed

+202
-60
lines changed

.github/workflows/backward-compatibility.yml

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -119,18 +119,29 @@ jobs:
119119
mkdir artifacts
120120
121121
- name: Run E2E tests
122-
shell: bash
123-
run: |
124-
./mithril-binaries/e2e/mithril-end-to-end -vvv \
125-
--bin-directory ./mithril-binaries/e2e \
126-
--work-directory=./artifacts \
127-
--devnet-scripts-directory=./mithril-test-lab/mithril-devnet \
128-
--cardano-node-version ${{ matrix.cardano_node_version }} \
129-
--cardano-slot-length 0.25 \
130-
--cardano-epoch-length 45.0 \
131-
--signed-entity-types ${{ needs.prepare-env-variables.outputs.signed-entity-types }} \
132-
&& echo "SUCCESS=true" >> $GITHUB_ENV \
133-
|| (echo "SUCCESS=false" >> $GITHUB_ENV && exit 1)
122+
uses: nick-fields/retry@v3
123+
with:
124+
shell: bash
125+
max_attempts: 3
126+
retry_on_exit_code: 2
127+
timeout_minutes: 10
128+
warning_on_retry: true
129+
command: |
130+
./mithril-binaries/e2e/mithril-end-to-end -vvv \
131+
--bin-directory ./mithril-binaries/e2e \
132+
--work-directory=./artifacts \
133+
--devnet-scripts-directory=./mithril-test-lab/mithril-devnet \
134+
--cardano-node-version ${{ matrix.cardano_node_version }} \
135+
--cardano-slot-length 0.25 \
136+
--cardano-epoch-length 45.0 \
137+
--signed-entity-types ${{ needs.prepare-env-variables.outputs.signed-entity-types }}
138+
EXIT_CODE=$?
139+
if [ $EXIT_CODE -eq 0 ]; then
140+
echo "SUCCESS=true" >> $GITHUB_ENV
141+
else
142+
echo "SUCCESS=false" >> $GITHUB_ENV
143+
fi
144+
exit $EXIT_CODE
134145
135146
- name: Define the JSON file name for the test result
136147
shell: bash

.github/workflows/ci.yml

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -347,25 +347,34 @@ jobs:
347347
mkdir artifacts
348348
349349
- name: Test
350-
run: |
351-
cat > ./mithril-end-to-end.sh << EOF
352-
#!/bin/bash
353-
set -x
354-
./mithril-end-to-end -vvv \\
355-
--bin-directory ./bin \\
356-
--work-directory=./artifacts \\
357-
--devnet-scripts-directory=./mithril-test-lab/mithril-devnet \\
358-
--mithril-era=${{ matrix.era }} \\
359-
--cardano-node-version ${{ matrix.cardano_node_version }} \\
360-
--cardano-hard-fork-latest-era-at-epoch ${{ matrix.hard_fork_latest_era_at_epoch }} ${{ matrix.extra_args }} \\
361-
EOF
362-
# If there is a next era, we need to specify it with '--mithril-next-era'
363-
if [[ "${{ matrix.next_era }}" != "" ]]; then
364-
echo " --mithril-next-era=${{ matrix.next_era }}" >> ./mithril-end-to-end.sh
365-
fi
366-
chmod u+x ./mithril-end-to-end.sh
367-
./mithril-end-to-end.sh
368-
rm ./mithril-end-to-end.sh
350+
uses: nick-fields/retry@v3
351+
with:
352+
shell: bash
353+
max_attempts: 3
354+
retry_on_exit_code: 2
355+
timeout_minutes: 10
356+
warning_on_retry: true
357+
command: |
358+
cat > ./mithril-end-to-end.sh << EOF
359+
#!/bin/bash
360+
set -x
361+
./mithril-end-to-end -vvv \\
362+
--bin-directory ./bin \\
363+
--work-directory=./artifacts \\
364+
--devnet-scripts-directory=./mithril-test-lab/mithril-devnet \\
365+
--mithril-era=${{ matrix.era }} \\
366+
--cardano-node-version ${{ matrix.cardano_node_version }} \\
367+
--cardano-hard-fork-latest-era-at-epoch ${{ matrix.hard_fork_latest_era_at_epoch }} ${{ matrix.extra_args }} \\
368+
EOF
369+
# If there is a next era, we need to specify it with '--mithril-next-era'
370+
if [[ "${{ matrix.next_era }}" != "" ]]; then
371+
echo " --mithril-next-era=${{ matrix.next_era }}" >> ./mithril-end-to-end.sh
372+
fi
373+
chmod u+x ./mithril-end-to-end.sh
374+
./mithril-end-to-end.sh
375+
EXIT_CODE=$?
376+
rm ./mithril-end-to-end.sh
377+
exit $EXIT_CODE
369378
370379
- name: Upload E2E Tests Artifacts
371380
if: ${{ failure() }}

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mithril-test-lab/mithril-end-to-end/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mithril-end-to-end"
3-
version = "0.4.49"
3+
version = "0.4.50"
44
authors = { workspace = true }
55
edition = { workspace = true }
66
documentation = { workspace = true }
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
mod runner;
22

3-
pub use runner::{Devnet, DevnetBootstrapArgs, DevnetTopology, PoolNode};
3+
pub use runner::{Devnet, DevnetBootstrapArgs, DevnetTopology, PoolNode, RetryableDevnetError};

mithril-test-lab/mithril-end-to-end/src/devnet/runner.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ use std::fs::{self, read_to_string, File};
66
use std::io::Read;
77
use std::path::{Path, PathBuf};
88
use std::process::Stdio;
9+
use thiserror::Error;
910
use tokio::process::Command;
1011

12+
#[derive(Error, Debug, PartialEq, Eq)]
13+
#[error("Retryable devnet error: `{0}`")]
14+
pub struct RetryableDevnetError(pub String);
15+
1116
#[derive(Debug, Clone, Default)]
1217
pub struct Devnet {
1318
artifacts_dir: PathBuf,
@@ -211,7 +216,9 @@ impl Devnet {
211216
.with_context(|| "Error while starting the devnet")?;
212217
match status.code() {
213218
Some(0) => Ok(()),
214-
Some(code) => Err(anyhow!("Run devnet exited with status code: {code}")),
219+
Some(code) => Err(anyhow!(RetryableDevnetError(format!(
220+
"Run devnet exited with status code: {code}"
221+
)))),
215222
None => Err(anyhow!("Run devnet terminated by signal")),
216223
}
217224
}
@@ -258,7 +265,9 @@ impl Devnet {
258265
.with_context(|| "Error while delegating stakes to the pools")?;
259266
match status.code() {
260267
Some(0) => Ok(()),
261-
Some(code) => Err(anyhow!("Delegating stakes exited with status code: {code}")),
268+
Some(code) => Err(anyhow!(RetryableDevnetError(format!(
269+
"Delegating stakes exited with status code: {code}"
270+
)))),
262271
None => Err(anyhow!("Delegating stakes terminated by signal")),
263272
}
264273
}
@@ -282,9 +291,9 @@ impl Devnet {
282291
.with_context(|| "Error while writing era marker on chain")?;
283292
match status.code() {
284293
Some(0) => Ok(()),
285-
Some(code) => Err(anyhow!(
294+
Some(code) => Err(anyhow!(RetryableDevnetError(format!(
286295
"Write era marker on chain exited with status code: {code}"
287-
)),
296+
)))),
288297
None => Err(anyhow!("Write era marker on chain terminated by signal")),
289298
}
290299
}
@@ -308,9 +317,9 @@ impl Devnet {
308317
.with_context(|| "Error while to transferring funds on chain")?;
309318
match status.code() {
310319
Some(0) => Ok(()),
311-
Some(code) => Err(anyhow!(
320+
Some(code) => Err(anyhow!(RetryableDevnetError(format!(
312321
"Transfer funds on chain exited with status code: {code}"
313-
)),
322+
)))),
314323
None => Err(anyhow!("Transfer funds on chain terminated by signal")),
315324
}
316325
}

mithril-test-lab/mithril-end-to-end/src/main.rs

Lines changed: 133 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
use anyhow::{anyhow, Context};
22
use clap::{CommandFactory, Parser, Subcommand};
33
use slog::{Drain, Level, Logger};
4-
use slog_scope::{error, info, warn};
4+
use slog_scope::{error, info};
55
use std::{
6-
fs,
6+
fmt, fs,
77
path::{Path, PathBuf},
8+
process::{ExitCode, Termination},
89
sync::Arc,
910
time::Duration,
1011
};
12+
use thiserror::Error;
1113
use tokio::{
1214
signal::unix::{signal, SignalKind},
1315
sync::Mutex,
@@ -17,7 +19,8 @@ use tokio::{
1719
use mithril_common::StdResult;
1820
use mithril_doc::GenerateDocCommands;
1921
use mithril_end_to_end::{
20-
Devnet, DevnetBootstrapArgs, MithrilInfrastructure, MithrilInfrastructureConfig, RunOnly, Spec,
22+
Devnet, DevnetBootstrapArgs, MithrilInfrastructure, MithrilInfrastructureConfig,
23+
RetryableDevnetError, RunOnly, Spec,
2124
};
2225

2326
/// Tests args
@@ -152,8 +155,16 @@ enum EndToEndCommands {
152155
GenerateDoc(GenerateDocCommands),
153156
}
154157

155-
#[tokio::main]
156-
async fn main() -> StdResult<()> {
158+
fn main() -> AppResult {
159+
tokio::runtime::Builder::new_multi_thread()
160+
.enable_all()
161+
.build()
162+
.unwrap()
163+
.block_on(async { main_exec().await })
164+
.into()
165+
}
166+
167+
async fn main_exec() -> StdResult<()> {
157168
let args = Args::parse();
158169
let _guard = slog_scope::set_global_logger(build_logger(&args));
159170

@@ -198,9 +209,69 @@ async fn main() -> StdResult<()> {
198209

199210
app_stopper.stop().await;
200211
join_set.shutdown().await;
212+
201213
res
202214
}
203215

216+
#[derive(Debug)]
217+
enum AppResult {
218+
Success(),
219+
UnretryableError(anyhow::Error),
220+
RetryableError(anyhow::Error),
221+
Cancelled(anyhow::Error),
222+
}
223+
224+
impl AppResult {
225+
fn exit_code(&self) -> ExitCode {
226+
match self {
227+
AppResult::Success() => ExitCode::SUCCESS,
228+
AppResult::UnretryableError(_) | AppResult::Cancelled(_) => ExitCode::FAILURE,
229+
AppResult::RetryableError(_) => ExitCode::from(2),
230+
}
231+
}
232+
}
233+
234+
impl fmt::Display for AppResult {
235+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
236+
match self {
237+
AppResult::Success() => write!(f, "Success"),
238+
AppResult::UnretryableError(error) => write!(f, "Error(Unretryable): {error:?}"),
239+
AppResult::RetryableError(error) => write!(f, "Error(Retryable): {error:?}"),
240+
AppResult::Cancelled(error) => write!(f, "Cancelled: {error:?}"),
241+
}
242+
}
243+
}
244+
245+
impl Termination for AppResult {
246+
fn report(self) -> ExitCode {
247+
let exit_code = self.exit_code();
248+
println!(" ");
249+
println!("{:-^100}", "");
250+
println!("Mithril End to End test outcome:");
251+
println!("{:-^100}", "");
252+
println!("{self}");
253+
254+
exit_code
255+
}
256+
}
257+
258+
impl From<StdResult<()>> for AppResult {
259+
fn from(result: StdResult<()>) -> Self {
260+
match result {
261+
Ok(()) => AppResult::Success(),
262+
Err(error) => {
263+
if error.is::<RetryableDevnetError>() {
264+
AppResult::RetryableError(error)
265+
} else if error.is::<SignalError>() {
266+
AppResult::Cancelled(error)
267+
} else {
268+
AppResult::UnretryableError(error)
269+
}
270+
}
271+
}
272+
}
273+
}
274+
204275
struct App {
205276
devnet: Arc<Mutex<Option<Devnet>>>,
206277
infrastructure: Arc<Mutex<Option<MithrilInfrastructure>>>,
@@ -338,31 +409,73 @@ fn create_workdir_if_not_exist_clean_otherwise(work_dir: &Path) {
338409
fs::create_dir(work_dir).expect("Work dir creation failure");
339410
}
340411

412+
#[derive(Error, Debug, PartialEq, Eq)]
413+
#[error("Signal received: `{0}`")]
414+
pub struct SignalError(pub String);
415+
341416
fn with_gracefull_shutdown(join_set: &mut JoinSet<StdResult<()>>) {
342417
join_set.spawn(async move {
343418
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to create SIGTERM signal");
344-
sigterm
345-
.recv()
346-
.await
347-
.ok_or(anyhow!("Failed to receive SIGTERM"))
348-
.inspect(|()| warn!("Received SIGTERM"))
419+
sigterm.recv().await;
420+
421+
Err(anyhow!(SignalError("SIGTERM".to_string())))
349422
});
350423

351424
join_set.spawn(async move {
352425
let mut sigterm = signal(SignalKind::interrupt()).expect("Failed to create SIGINT signal");
353-
sigterm
354-
.recv()
355-
.await
356-
.ok_or(anyhow!("Failed to receive SIGINT"))
357-
.inspect(|()| warn!("Received SIGINT"))
426+
sigterm.recv().await;
427+
428+
Err(anyhow!(SignalError("SIGINT".to_string())))
358429
});
359430

360431
join_set.spawn(async move {
361432
let mut sigterm = signal(SignalKind::quit()).expect("Failed to create SIGQUIT signal");
362-
sigterm
363-
.recv()
364-
.await
365-
.ok_or(anyhow!("Failed to receive SIGQUIT"))
366-
.inspect(|()| warn!("Received SIGQUIT"))
433+
sigterm.recv().await;
434+
435+
Err(anyhow!(SignalError("SIGQUIT".to_string())))
367436
});
368437
}
438+
439+
#[cfg(test)]
440+
mod tests {
441+
use super::*;
442+
443+
#[test]
444+
fn app_result_exit_code() {
445+
let expected_exit_code = ExitCode::SUCCESS;
446+
let exit_code = AppResult::Success().exit_code();
447+
assert_eq!(expected_exit_code, exit_code);
448+
449+
let expected_exit_code = ExitCode::FAILURE;
450+
let exit_code = AppResult::UnretryableError(anyhow::anyhow!("an error")).exit_code();
451+
assert_eq!(expected_exit_code, exit_code);
452+
453+
let expected_exit_code = ExitCode::from(2);
454+
let exit_code = AppResult::RetryableError(anyhow::anyhow!("an error")).exit_code();
455+
assert_eq!(expected_exit_code, exit_code);
456+
457+
let expected_exit_code = ExitCode::FAILURE;
458+
let exit_code = AppResult::Cancelled(anyhow::anyhow!("an error")).exit_code();
459+
assert_eq!(expected_exit_code, exit_code);
460+
}
461+
462+
#[test]
463+
fn app_result_conversion() {
464+
assert!(matches!(AppResult::from(Ok(())), AppResult::Success()));
465+
466+
assert!(matches!(
467+
AppResult::from(Err(anyhow!(RetryableDevnetError("an error".to_string())))),
468+
AppResult::RetryableError(_)
469+
));
470+
471+
assert!(matches!(
472+
AppResult::from(Err(anyhow!("an error"))),
473+
AppResult::UnretryableError(_)
474+
));
475+
476+
assert!(matches!(
477+
AppResult::from(Err(anyhow!(SignalError("an error".to_string())))),
478+
AppResult::Cancelled(_)
479+
));
480+
}
481+
}

0 commit comments

Comments
 (0)