Bug: [CYPRESS_FRAMEWORK] Run Cypress tests in parallel



**Description:**

Cypress tests are resource intensive and consume a lot of memory and power. But that should not stop us from running them in parallel even if they consume a lot of resources.
At present, Cypress tests here at Hyperswitch is run in sequential and consumes a lot of time given that each connector consumes at least 10+ mins and we've 10+ connector to run tests for.

With that said, the task is to parallelize the tests that are run and with that said, the consumption can be reduced drastically (1+ hour(s) to mere 15 mins).

> [!IMPORTANT]
> **Cypress cloud is a no go**

Below is the shell script that runs helps run the test in both sequential and parallel ways:

```sh
#!/bin/bash

CONNECTORS=("adyen" "bankofamerica" "bluesnap" "cybersource" "iatapay" "nmi" "paypal" "stripe" "trustpay")

# Function to run tests
run_tests() {
  mkdir -p results
  local pids=()

  for connector in "${CONNECTORS[@]}"; do
    if [[ $parallel == true ]]; then
      CYPRESS_CONNECTOR="${connector}" npm run cypress:ci > results/${connector}.txt 2>&1 &
      pids+=($!)
    else
      printf "\rRunning Cypress tests against the connector: ${connector}"
      CYPRESS_CONNECTOR="${connector}" npm run cypress:ci > results/${connector}.txt 2>&1
      local exit_code=$?
      if [[ $exit_code -ne 0 && $exit_code -ge 128 ]]; then
        exit 1
      fi

      sleep 3
    fi
  done

  if [[ $parallel == true ]]; then
    for pid in "${pids[@]}"; do
      wait $pid
    done
  fi
}

# Check if running in parallel
if [[ "${1}" == "--parallel" ]]; then
  printf "WARNING: Parallel tests are flaky and may not work as expected.\n"
  eval run_tests true
else
  eval run_tests false
fi

```

To execute the script:

```sh
# to run tests sequentially, which can take an hour+
sh run-cypress.sh
# to run tests in parallel, consumes around 15 - 20 mins, at the cost of resources
sh run-cypress.sh --parallel
```

**Getting started:**

Go through the [README](https://github.com/juspay/hyperswitch/blob/main/cypress-tests/readme.md) for guidelines on file structure, formatting and instructions on how to set up and run Cypress.

In short:

```sh
cd cypress-tests
npm ci
```

**Possible implementation:**

- Shell script above is attached for reference
- Make use of `gnu-parallel` to reduce the complexity of the script. The advantage here is that, it also make the script more future proof, manageable, easier to read and understand
- As a bonus, the same functionality can be implemented in JavaScript directly into Cypress via its configs is a plus

>[!WARNING]
> **Make sure that before this task is picked, the machine is capable enough to run at least 2 connector tests in parallel**. Running Cypress tests in parallel is too resource intensive!
> Please go through the Cypress [docs](https://docs.cypress.io/guides/getting-started/installing-cypress#CPU) to learn whether you're machine meets minimum requirements.


diff --git a/scripts/execute_cypress.sh b/scripts/execute_cypress.sh
new file mode 100755
index 00000000000..1f1219ee717
--- /dev/null
+++ b/scripts/execute_cypress.sh
@@ -0,0 +1,188 @@
+#! /usr/bin/env bash
+
+set -euo pipefail
+
+# Initialize tmp_file globally
+tmp_file=""
+
+# Define arrays for services, etc.
+# Read service arrays from environment variables
+read -r -a payments <<< "${PAYMENTS_CONNECTORS[@]:-}"
+read -r -a payouts <<< "${PAYOUTS_CONNECTORS[@]:-}"
+read -r -a payment_method_list <<< "${PAYMENT_METHOD_LIST[@]:-}"
+read -r -a routing <<< "${ROUTING[@]:-}"
+
+# Define arrays
+connector_map=()
+failed_connectors=()
+
+# Define an associative array to map environment variables to service names
+declare -A services=(
+  ["PAYMENTS_CONNECTORS"]="payments"
+  ["PAYOUTS_CONNECTORS"]="payouts"
+  ["PAYMENT_METHOD_LIST"]="payment_method_list"
+  ["ROUTING"]="routing"
+)
+
+# Function to print messages in color
+function print_color() {
+  # Input params
+  local color="$1"
+  local message="$2"
+
+  # Define colors
+  local reset='\033[0m'
+  local red='\033[0;31m'
+  local green='\033[0;32m'
+  local yellow='\033[0;33m'
+
+  # Use indirect reference to get the color value
+  echo -e "${!color}${message}${reset}"
+}
+export -f print_color
+
+# Function to check if a command exists
+function command_exists() {
+  command -v "$1" > /dev/null 2>&1
+}
+
+# Function to read service arrays from environment variables
+function read_service_arrays() {
+  # Loop through the associative array and check if each service is exported
+  for var in "${!services[@]}"; do
+    if [[ -n "${!var+x}" ]]; then
+      connector_map+=("${services[$var]}")
+    else
+      print_color "yellow" "Environment variable ${var} is not set. Skipping..."
+    fi
+  done
+}
+
+# Function to execute Cypress tests
+function execute_test() {
+  if [[ $# -lt 3 ]]; then
+    print_color "red" "ERROR: Insufficient arguments provided to execute_test."
+    exit 1
+  fi
+
+  local connector="$1"
+  local service="$2"
+  local tmp_file="$3"
+
+  print_color "yellow" "Executing tests for ${service} with connector ${connector}..."
+
+  export REPORT_NAME="${service}_${connector}_report"
+
+  if ! CYPRESS_CONNECTOR="$connector" npm run "cypress:$service"; then
+    echo "${service}-${connector}" >> "${tmp_file}"
+  fi
+}
+export -f execute_test
+
+# Function to run tests
+function run_tests() {
+  local jobs="${1:-1}"
+  tmp_file=$(mktemp)
+
+  # Ensure temporary file is removed on script exit
+  trap 'cleanup' EXIT
+
+  for service in "${connector_map[@]}"; do
+    declare -n connectors="$service"
+
+    if [[ ${#connectors[@]} -eq 0 ]]; then
+      # Service-level test (e.g., payment-method-list or routing)
+      [[ $service == "payment_method_list" ]] && service="payment-method-list"
+
+      echo "Running ${service} tests without connectors..."
+      export REPORT_NAME="${service}_report"
+
+      if ! npm run "cypress:${service}"; then
+        echo "${service}" >> "${tmp_file}"
+      fi
+    else
+      # Connector-specific tests (e.g., payments or payouts)
+      print_color "yellow" "Running tests for service: '${service}' with connectors: [${connectors[*]}] in batches of ${jobs}..."
+
+      # Execute tests in parallel
+      printf '%s\n' "${connectors[@]}" | parallel --jobs "${jobs}" execute_test {} "${service}" "${tmp_file}"
+    fi
+  done
+
+  # Collect failed connectors
+  if [[ -s "${tmp_file}" ]]; then
+    failed_connectors=($(< "${tmp_file}"))
+    print_color "red" "One or more connectors failed to run:"
+    printf '%s\n' "${failed_connectors[@]}"
+    exit 1
+  else
+    print_color "green" "Cypress tests execution successful!"
+  fi
+}
+
+# Function to check and install dependencies
+function check_dependencies() {
+  # parallel and npm are mandatory dependencies. exit the script if not found.
+  local dependencies=("parallel" "npm")
+
+  for cmd in "${dependencies[@]}"; do
+    if ! command_exists "$cmd"; then
+      print_color "red" "ERROR: ${cmd^} is not installed!"
+      exit 1
+    else
+      print_color "green" "${cmd^} is installed already!"
+
+      if [[ ${cmd} == "npm" ]]; then
+        npm ci || {
+          print_color "red" "Command \`npm ci\` failed!"
+          exit 1
+        }
+      fi
+    fi
+  done
+}
+
+# Cleanup function to handle exit
+function cleanup() {
+  print_color "yellow" "Cleaning up..."
+  if [[ -d "cypress-tests" ]]; then
+    cd -
+  fi
+
+  if [[ -n "${tmp_file}" && -f "${tmp_file}" ]]; then
+    rm -f "${tmp_file}"
+  fi
+}
+
+# Main function
+function main() {
+  local command="${1:-}"
+  local jobs="${2:-5}"
+
+  # Ensure script runs from 'cypress-tests' directory
+  if [[ "$(basename "$PWD")" != "cypress-tests" ]]; then
+    print_color "yellow" "Changing directory to 'cypress-tests'..."
+    cd cypress-tests || {
+      print_color "red" "ERROR: Directory 'cypress-tests' not found!"
+      exit 1
+    }
+  fi
+
+  check_dependencies
+  read_service_arrays
+
+  case "$command" in
+    --parallel | -p)
+      print_color "yellow" "WARNING: Running Cypress tests in parallel is more resource-intensive!"
+      # At present, parallel execution is restricted to not run out of memory
+      # But can be scaled up by passing the value as an argument
+      run_tests "$jobs"
+      ;;
+    *)
+      run_tests 1
+      ;;
+  esac
+}
+
+# Execute the main function with passed arguments
+main "$@"

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [ ] New feature
- [x] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [x] CI/CD

## Description
<!-- Describe your changes in detail -->

This PR makes it possible for us to run Cypress tests in parallel by leveraging `gnu-parallel`.
At present, we'll only be running against 5 connectors namely,
- payments: bluesnap, cybersource, stripe
- payouts: adyen and wise

w.r.t pml and other connectors, there are some failures within the tests that needs to be addressed.

>[!IMPORTANT]
> Cypress in parallel is resource heavy and running it in batch of 3+ will result in unintended consequences like abrupt failures due to resource limitation.

Running Cypress tests in parallel also puts up a challenge where when a specific test fails for 2 connectors at the same time, test against both the connectors stops abruptly. This is because of the file name conflict as we not have the privilege to change the names of the screenshots while capturing but only after the screenshot is captured.

To address this, the format of screenshot has been changed where screenshot directories are changed based on the connectors (just because we now have the privilege to change directories in real time).

Also, a similar issue with reports have also been addressed.

Documentation has been updated to address the usage of `scripts/execute_cypress.sh` script.
In short,

```sh
source .env
scripts/execute_cypress.sh --parallel <jobs_count>
```

>[!NOTE]
> make sure that service variables (PAYMENTS, PAYOUTS, PAYMENT_METHOD_LIST, ROUTING) is exported along with CYPRESS env variables in the `.env` file. Check README for more info.
> if `--parallel` is not passed, tests will run in sequential manner by setting jobs to `1`

### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->

When Cypress tests are run in sequential, it takes about `1h10m+` to run against `10+` prod connectors. With `parallel`, we'll bring that down to nearly `15m`.

This should close https://github.com/juspay/hyperswitch/issues/5914

## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->

Tested locally:

These are known failures.

<img width="576" alt="image" src="https://github.com/user-attachments/assets/12788d3f-f816-4395-9799-316546595823">

The ones that passed is `Stripe` and hence it is not in `failed_connector` list.

https://github.com/juspay/hyperswitch/actions/runs/11177572035/job/31077505520?pr=6225
https://github.com/juspay/hyperswitch/actions/runs/11384335020/job/31673789059

## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `prettier . --write`
- [ ] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
