diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 5e1bac239..4a61665da 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -28,10 +28,24 @@ jobs: uses: Swatinem/rust-cache@v2 with: cache-on-failure: true + cache-all-crates: true # due to candid-extractor - name: Install dfx uses: dfinity/setup-dfx@main + - name: Get PR number test + if: success() || failure() + env: + GH_TOKEN: ${{ github.token }} + run: | + echo "PR_NUMBER=$(gh pr list \ + --repo ${{ github.repository }} \ + --state open \ + --head ${{ github.ref }} \ + --base master \ + --json number \ + -q '.[0].number')" + - name: Build apps run: | ./apps/kv-store/build.sh @@ -45,7 +59,39 @@ jobs: run: | cargo build -p meroctl -p merod -p e2e-tests + - name: Prepare e2e-tests config + id: prepare_e2e_tests_config + run: | + # Generate 4 unique random numbers + random_numbers=() + while [ ${#random_numbers[@]} -lt 3 ]; do + num=$((RANDOM%37001 + 3000)) + if [[ ! " ${random_numbers[@]} " =~ " ${num} " ]]; then + random_numbers+=($num) + fi + done + + # Export random numbers to environment variables + echo "SWARM_PORT=${random_numbers[0]}" + echo "SERVER_PORT=${random_numbers[1]}" + echo "ICP_PORT=${random_numbers[2]}" + + # Update JSON file with jq + jq --arg swarmPort "${random_numbers[0]}" \ + --arg serverPort "${random_numbers[1]}" \ + --arg icpPort "${random_numbers[2]}" \ + '.network.startSwarmPort = ($swarmPort | tonumber) | + .network.startServerPort = ($serverPort | tonumber) | + .protocolSandboxes[1].config.rpcUrl = "http://127.0.0.1:\($icpPort)" + ' e2e-tests/config/config.json > updated_config.json + + mv updated_config.json e2e-tests/config/config.json + + echo "ICP_PORT=$ICP_PORT" >> $GITHUB_OUTPUT + - name: Deploy ICP local devnet + env: + ICP_PORT: ${{ steps.prepare_e2e_tests_config.outputs.ICP_PORT }} run: | cargo install candid-extractor cd ./contracts/icp/context-config @@ -60,39 +106,37 @@ jobs: export SWARM_HOST=$(ifconfig | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | head -n 1) echo "Running e2e tests, check job summary for details" - echo "# E2E tests 🏗️" >> $GITHUB_STEP_SUMMARY ./target/debug/e2e-tests \ --input-dir ./e2e-tests/config \ --output-dir ./e2e-tests/corpus \ --merod-binary ./target/debug/merod \ --meroctl-binary ./target/debug/meroctl - - name: Update pull request comment + - name: Get PR number + id: pr_number if: success() || failure() + env: + GH_TOKEN: ${{ github.token }} + run: | + echo "PR_NUMBER=$(gh pr list \ + --repo ${{ github.repository }} \ + --state open \ + --head ${{ github.ref }} \ + --base master \ + --json number \ + -q '.[0].number')" >> $GITHUB_OUTPUT + + - name: Update pull request comment + if: (success() || failure()) && steps.pr_number.outputs.PR_NUMBER != '' uses: thollander/actions-comment-pull-request@v3 with: file-path: ./e2e-tests/corpus/report.md + pr-number: ${{ steps.pr_number.outputs.PR_NUMBER }} - - name: Show node logs + - name: Upload artifacts if: success() || failure() - run: | - LOGS_DIR=./e2e-tests/corpus/logs - if [ ! -d "$LOGS_DIR" ]; then - echo "Directory $LOGS_DIR does not exist." - exit 1 - fi - - echo "# Node logs 📋" >> $GITHUB_STEP_SUMMARY - - for FOLDER in "$LOGS_DIR"/*; do - if [ -d "$FOLDER" ]; then - - RUN_LOG="$FOLDER/run.log" - if [ -f "$RUN_LOG" ]; then - echo "## Node logs: $(basename $FOLDER) 📋" >> $GITHUB_STEP_SUMMARY - cat "$RUN_LOG" >> $GITHUB_STEP_SUMMARY - else - echo "## No run.log found in $FOLDER ⚠️" >> $GITHUB_STEP_SUMMARY - fi - fi - done + uses: actions/upload-artifact@v4 + with: + name: e2e-tests-corpus + path: e2e-tests/corpus/ + retention-days: 2 diff --git a/contracts/icp/context-config/deploy_devnet.sh b/contracts/icp/context-config/deploy_devnet.sh index 668f9ebbd..4162d509a 100755 --- a/contracts/icp/context-config/deploy_devnet.sh +++ b/contracts/icp/context-config/deploy_devnet.sh @@ -80,7 +80,7 @@ RECIPIENT_PRINCIPAL=$(dfx identity get-principal) dfx identity use default # Start dfx with clean state -dfx start --clean --background +dfx start --clean --background --host 127.0.0.1:${ICP_PORT:-4943} # Create initial identity if needed dfx identity new --storage-mode=plaintext minting || true diff --git a/e2e-tests/src/driver.rs b/e2e-tests/src/driver.rs index 42e40d6da..2da0a98ff 100644 --- a/e2e-tests/src/driver.rs +++ b/e2e-tests/src/driver.rs @@ -59,18 +59,18 @@ impl<'a> TestContext<'a> { pub struct Driver { environment: TestEnvironment, config: Config, - meroctl: Meroctl, - merods: HashMap, +} + +pub struct Mero { + ctl: Meroctl, + ds: HashMap, } impl Driver { pub fn new(environment: TestEnvironment, config: Config) -> Self { - let meroctl = Meroctl::new(&environment); Self { environment, config, - meroctl, - merods: HashMap::new(), } } @@ -96,9 +96,11 @@ impl Driver { self.environment .output_writer .write_header(&format!("Running protocol {}", sandbox.name()), 1); - self.boot_merods(sandbox).await?; - report = self.run_scenarios(report, sandbox.name()).await?; - self.stop_merods().await; + + let mero = self.setup_mero(sandbox).await?; + report = self.run_scenarios(&mero, report, sandbox.name()).await?; + self.stop_merods(&mero.ds).await; + self.environment .output_writer .write_header(&format!("Finished protocol {}", sandbox.name()), 1); @@ -122,14 +124,16 @@ impl Driver { report.result() } - async fn boot_merods(&mut self, sandbox: &ProtocolSandboxEnvironment) -> EyreResult<()> { + async fn setup_mero(&mut self, sandbox: &ProtocolSandboxEnvironment) -> EyreResult { self.environment .output_writer .write_header("Starting merod nodes", 2); + let mut merods = HashMap::new(); + for i in 0..self.config.network.node_count { let node_name = format!("node{}", i + 1); - if !self.merods.contains_key(&node_name) { + if !merods.contains_key(&node_name) { let mut args = vec![format!( "discovery.rendezvous.namespace=\"calimero/e2e-tests/{}\"", self.environment.test_id @@ -140,7 +144,13 @@ impl Driver { let mut config_args = vec![]; config_args.extend(args.iter().map(|arg| &**arg)); - let merod = Merod::new(node_name.clone(), &self.environment); + let merod = Merod::new( + node_name.clone(), + self.environment.nodes_dir.join(sandbox.name()), + self.environment.logs_dir.join(sandbox.name()), + self.environment.merod_binary.clone(), + self.environment.output_writer, + ); let swarm_host = match env::var(&self.config.network.swarm_host_env) { Ok(host) => host, @@ -158,28 +168,34 @@ impl Driver { merod.run().await?; - drop(self.merods.insert(node_name, merod)); + drop(merods.insert(node_name, merod)); } } // TODO: Implement health check? sleep(Duration::from_secs(10)).await; - Ok(()) + Ok(Mero { + ctl: Meroctl::new( + self.environment.nodes_dir.join(sandbox.name()), + self.environment.meroctl_binary.clone(), + self.environment.output_writer, + ), + ds: merods, + }) } - async fn stop_merods(&mut self) { - for (_, merod) in self.merods.iter() { + async fn stop_merods(&mut self, merods: &HashMap) { + for (_, merod) in merods.iter() { if let Err(err) = merod.stop().await { eprintln!("Error stopping merod: {:?}", err); } } - - self.merods.clear(); } async fn run_scenarios( &self, + mero: &Mero, mut report: TestRunReport, protocol_name: String, ) -> EyreResult { @@ -193,6 +209,7 @@ impl Driver { if test_file_path.exists() { let scenario_report = self .run_scenario( + mero, path.file_name() .ok_or_eyre("failed")? .to_str() @@ -217,6 +234,7 @@ impl Driver { async fn run_scenario( &self, + mero: &Mero, scenarion_name: &str, file_path: PathBuf, ) -> EyreResult { @@ -233,7 +251,7 @@ impl Driver { .output_writer .write_string(format!("Steps count: {}", scenario.steps.len())); - let (inviter, invitees) = match self.pick_inviter_node() { + let (inviter, invitees) = match self.pick_inviter_node(&mero.ds) { Some((inviter, invitees)) => (inviter, invitees), None => bail!("Not enough nodes to run the test"), }; @@ -245,12 +263,8 @@ impl Driver { .output_writer .write_string(format!("Picked invitees: {:?}", invitees)); - let mut ctx = TestContext::new( - inviter, - invitees, - &self.meroctl, - self.environment.output_writer, - ); + let mut ctx = + TestContext::new(inviter, invitees, &mero.ctl, self.environment.output_writer); let mut report = TestScenarioReport::new(scenarion_name.to_owned()); @@ -271,8 +285,8 @@ impl Driver { Ok(report) } - fn pick_inviter_node(&self) -> Option<(String, Vec)> { - let mut node_names: Vec = self.merods.keys().cloned().collect(); + fn pick_inviter_node(&self, merods: &HashMap) -> Option<(String, Vec)> { + let mut node_names: Vec = merods.keys().cloned().collect(); if node_names.len() < 1 { None } else { @@ -357,11 +371,11 @@ impl TestRunReport { .steps .iter() .find(|step| &step.step_name == step_name) - .map_or(":interrobang:", |step| { + .map_or("N/A", |step| { if step.result.is_ok() { - ":white_check_mark:" + "Success" } else { - ":x:" + "Failure" } }); markdown.push_str(&format!(" {} |", result)); diff --git a/e2e-tests/src/meroctl.rs b/e2e-tests/src/meroctl.rs index f6d25440f..94ee57760 100644 --- a/e2e-tests/src/meroctl.rs +++ b/e2e-tests/src/meroctl.rs @@ -5,20 +5,19 @@ use eyre::{bail, eyre, OptionExt, Result as EyreResult}; use tokio::process::Command; use crate::output::OutputWriter; -use crate::TestEnvironment; pub struct Meroctl { - nodes_dir: Utf8PathBuf, + home_dir: Utf8PathBuf, binary: Utf8PathBuf, output_writer: OutputWriter, } impl Meroctl { - pub fn new(environment: &TestEnvironment) -> Self { + pub fn new(home_dir: Utf8PathBuf, binary: Utf8PathBuf, output_writer: OutputWriter) -> Self { Self { - nodes_dir: environment.nodes_dir.clone(), - binary: environment.meroctl_binary.clone(), - output_writer: environment.output_writer, + home_dir, + binary, + output_writer, } } @@ -142,7 +141,7 @@ impl Meroctl { async fn run_cmd(&self, node_name: &str, args: &[&str]) -> EyreResult { let mut root_args = vec![ "--home", - self.nodes_dir.as_str(), + self.home_dir.as_str(), "--node-name", node_name, "--output-format", diff --git a/e2e-tests/src/merod.rs b/e2e-tests/src/merod.rs index a0da0a1bc..0a8e51b68 100644 --- a/e2e-tests/src/merod.rs +++ b/e2e-tests/src/merod.rs @@ -8,26 +8,31 @@ use tokio::io::copy; use tokio::process::{Child, Command}; use crate::output::OutputWriter; -use crate::TestEnvironment; pub struct Merod { pub name: String, process: RefCell>, - nodes_dir: Utf8PathBuf, + home_dir: Utf8PathBuf, log_dir: Utf8PathBuf, binary: Utf8PathBuf, output_writer: OutputWriter, } impl Merod { - pub fn new(name: String, environment: &TestEnvironment) -> Self { + pub fn new( + name: String, + home_dir: Utf8PathBuf, + logs_dir: Utf8PathBuf, + binary: Utf8PathBuf, + output_writer: OutputWriter, + ) -> Self { Self { process: RefCell::new(None), - nodes_dir: environment.nodes_dir.clone(), - log_dir: environment.logs_dir.join(&name), - binary: environment.merod_binary.clone(), + home_dir, + log_dir: logs_dir.join(&name), + binary, name, - output_writer: environment.output_writer, + output_writer, } } @@ -38,7 +43,7 @@ impl Merod { server_port: u32, args: &[&str], ) -> EyreResult<()> { - create_dir_all(&self.nodes_dir.join(&self.name)).await?; + create_dir_all(&self.home_dir.join(&self.name)).await?; create_dir_all(&self.log_dir).await?; let mut child = self @@ -96,7 +101,7 @@ impl Merod { } async fn run_cmd(&self, args: &[&str], log_suffix: &str) -> EyreResult { - let mut root_args = vec!["--home", self.nodes_dir.as_str(), "--node-name", &self.name]; + let mut root_args = vec!["--home", self.home_dir.as_str(), "--node-name", &self.name]; root_args.extend(args);