From 737bddd6318817759ac51c2171e7aefdebb2808a Mon Sep 17 00:00:00 2001 From: spawn Date: Wed, 22 Apr 2026 11:27:23 +0300 Subject: [PATCH] ci: stabilize Jenkins jobs and mobile pipeline --- ansible/playbooks/site.yml | 9 +- compose/docker-compose.yml | 12 +- compose/images/slave-maven/Dockerfile | 1 + compose/images/test-selenium/Dockerfile | 56 ++- compose/jenkins/casc/jenkins.yaml | 2 +- config/compose/mobile-ci.compose.yml | 4 +- config/jobs-devops/global.yaml | 6 + .../templates/infra-health-check.yaml | 11 + .../jobs-devops/templates/jobs-uploader.yaml | 16 + config/jobs-devops/view.yaml | 18 + config/jobs/global.yaml | 2 +- config/jobs/scripts/infra-health-check.groovy | 19 +- config/jobs/scripts/jobs-uploader.groovy | 12 + .../jobs/scripts/qa-api-citrus-tests.groovy | 32 +- .../scripts/qa-mobile-appium-tests.groovy | 352 ++++++++++++++++-- config/jobs/scripts/qa-runner.groovy | 80 +++- config/jobs/scripts/qa-selenium-tests.groovy | 95 ++++- config/jobs/templates/jobs-uploader.yaml | 2 +- .../jobs/templates/qa-api-citrus-tests.yaml | 9 + .../templates/qa-mobile-appium-tests.yaml | 32 ++ config/jobs/templates/qa-runner.yaml | 24 +- config/jobs/templates/qa-selenium-tests.yaml | 14 +- config/jobs/view.yaml | 20 +- 23 files changed, 762 insertions(+), 66 deletions(-) create mode 100644 config/jobs-devops/global.yaml create mode 100644 config/jobs-devops/templates/infra-health-check.yaml create mode 100644 config/jobs-devops/templates/jobs-uploader.yaml create mode 100644 config/jobs-devops/view.yaml diff --git a/ansible/playbooks/site.yml b/ansible/playbooks/site.yml index 1ef12da..68139bc 100644 --- a/ansible/playbooks/site.yml +++ b/ansible/playbooks/site.yml @@ -7,6 +7,11 @@ compose_dir: "{{ hw8_root }}/compose" compose_file: "{{ compose_dir }}/docker-compose.yml" compose_env_file: "{{ compose_dir }}/.env" + jobs_profile_effective: "{{ jobs_profile | default('full') }}" + jjb_paths: + full: /workspace/hw8/config/jobs + devops: /workspace/hw8/config/jobs-devops + jjb_upload_path: "{{ jjb_paths.get(jobs_profile_effective, jjb_paths.full) }}" tasks: - name: Check Docker CLI availability @@ -89,7 +94,7 @@ - name: Upload Jenkins jobs via JJB container ansible.builtin.command: - cmd: timeout 900 docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} run --rm jobs_uploader + cmd: timeout 900 docker compose -f {{ compose_file }} --env-file {{ compose_env_file }} run --rm -e JJB_PATH={{ jjb_upload_path }} jobs_uploader args: chdir: "{{ compose_dir }}" @@ -98,3 +103,5 @@ msg: - "Jenkins UI: http://localhost:8088 (via nginx) or http://localhost:8081" - "Registry: http://localhost:5005/v2/" + - "JJB profile: {{ jobs_profile_effective }}" + - "JJB path: {{ jjb_upload_path }}" diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index 773cbec..2c1f5ca 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -33,7 +33,7 @@ services: - jenkins_home:/var/jenkins_home - ./jenkins/casc:/var/jenkins_home/casc_configs:ro - /var/run/docker.sock:/var/run/docker.sock - - ../..:/workspace/otus-autotests:ro + - ..:/workspace/hw8:ro healthcheck: test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8080/login >/dev/null"] interval: 10s @@ -67,9 +67,9 @@ services: JENKINS_HOSTNAME: http://jenkins:8080 JENKINS_USERNAME: ${JENKINS_ADMIN_ID} JENKINS_PASSWORD: ${JENKINS_ADMIN_PASSWORD} - JJB_PATH: /workspace/otus-autotests/hw8/config/jobs + JJB_PATH: /workspace/hw8/config/jobs volumes: - - ../..:/workspace/otus-autotests:ro + - ..:/workspace/hw8:ro networks: - jenkins_net @@ -87,9 +87,10 @@ services: JENKINS_AGENT_WORKDIR: /home/jenkins/agent JENKINS_WEB_SOCKET: "true" JENKINS_LABELS: maven docker + HW8_ROOT: /workspace/hw8 volumes: - /var/run/docker.sock:/var/run/docker.sock - - ../..:/workspace/otus-autotests:ro + - ..:/workspace/hw8:ro networks: - jenkins_net @@ -107,9 +108,10 @@ services: JENKINS_AGENT_WORKDIR: /home/jenkins/agent JENKINS_WEB_SOCKET: "true" JENKINS_LABELS: jjb docker + HW8_ROOT: /workspace/hw8 volumes: - /var/run/docker.sock:/var/run/docker.sock - - ../..:/workspace/otus-autotests:ro + - ..:/workspace/hw8:ro networks: - jenkins_net diff --git a/compose/images/slave-maven/Dockerfile b/compose/images/slave-maven/Dockerfile index 518b99e..ffeafc6 100644 --- a/compose/images/slave-maven/Dockerfile +++ b/compose/images/slave-maven/Dockerfile @@ -8,6 +8,7 @@ RUN apt-get update \ ca-certificates \ curl \ docker.io \ + git \ unzip \ && rm -rf /var/lib/apt/lists/* diff --git a/compose/images/test-selenium/Dockerfile b/compose/images/test-selenium/Dockerfile index 98b0240..2befbd6 100644 --- a/compose/images/test-selenium/Dockerfile +++ b/compose/images/test-selenium/Dockerfile @@ -1,9 +1,55 @@ FROM maven:3.9.11-eclipse-temurin-21 -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - chromium \ - chromium-driver \ - && rm -rf /var/lib/apt/lists/* +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + unzip \ + fontconfig \ + fonts-liberation \ + libasound2t64 \ + libatk-bridge2.0-0 \ + libatk1.0-0 \ + libc6 \ + libcairo2 \ + libcups2 \ + libdbus-1-3 \ + libexpat1 \ + libfontconfig1 \ + libgbm1 \ + libgcc1 \ + libglib2.0-0 \ + libgtk-3-0 \ + libnspr4 \ + libnss3 \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libstdc++6 \ + libx11-6 \ + libx11-xcb1 \ + libxcb1 \ + libxcomposite1 \ + libxcursor1 \ + libxdamage1 \ + libxext6 \ + libxfixes3 \ + libxi6 \ + libxrandr2 \ + libxrender1 \ + libxss1 \ + libxtst6 \ + xdg-utils; \ + CHROME_VERSION="$(curl -fsSL https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE)"; \ + curl -fsSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chrome-linux64.zip" -o /tmp/chrome.zip; \ + curl -fsSL "https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chromedriver-linux64.zip" -o /tmp/chromedriver.zip; \ + unzip -q /tmp/chrome.zip -d /opt; \ + unzip -q /tmp/chromedriver.zip -d /opt; \ + ln -sf /opt/chrome-linux64/chrome /usr/bin/google-chrome; \ + ln -sf /opt/chrome-linux64/chrome /usr/bin/chromium; \ + ln -sf /opt/chromedriver-linux64/chromedriver /usr/bin/chromedriver; \ + chmod +x /opt/chrome-linux64/chrome /opt/chromedriver-linux64/chromedriver; \ + rm -f /tmp/chrome.zip /tmp/chromedriver.zip; \ + rm -rf /var/lib/apt/lists/* WORKDIR /workspace diff --git a/compose/jenkins/casc/jenkins.yaml b/compose/jenkins/casc/jenkins.yaml index 9acd3f2..bdaffdf 100644 --- a/compose/jenkins/casc/jenkins.yaml +++ b/compose/jenkins/casc/jenkins.yaml @@ -30,7 +30,7 @@ jenkins: - key: JENKINS_URL_INTERNAL value: "http://jenkins:8080" - key: OTUS_WORKSPACE_ROOT - value: "/workspace/otus-autotests" + value: "/workspace/hw8" - key: MOBILE_DB_PASSWORD value: "${MOBILE_DB_PASSWORD}" diff --git a/config/compose/mobile-ci.compose.yml b/config/compose/mobile-ci.compose.yml index 32884a8..a2673e5 100644 --- a/config/compose/mobile-ci.compose.yml +++ b/config/compose/mobile-ci.compose.yml @@ -24,7 +24,7 @@ services: - EMULATOR_PARAMS=-no-window -no-audio -gpu swiftshader_indirect -no-snapshot -no-boot-anim shm_size: 2gb healthcheck: - test: ["CMD-SHELL", "[ \"$(cat /home/androidusr/device_status 2>/dev/null)\" = \"READY\" ]"] + test: ["CMD-SHELL", "[ \"$(cat /home/androidusr/device_status 2>/dev/null)\" = \"READY\" ] && wget -qO- http://127.0.0.1:4723/status >/dev/null 2>&1"] interval: 15s timeout: 5s retries: 40 @@ -45,7 +45,7 @@ services: - EMULATOR_PARAMS=-no-window -no-audio -gpu swiftshader_indirect -no-snapshot -no-boot-anim shm_size: 2gb healthcheck: - test: ["CMD-SHELL", "[ \"$(cat /home/androidusr/device_status 2>/dev/null)\" = \"READY\" ]"] + test: ["CMD-SHELL", "[ \"$(cat /home/androidusr/device_status 2>/dev/null)\" = \"READY\" ] && wget -qO- http://127.0.0.1:4723/status >/dev/null 2>&1"] interval: 15s timeout: 5s retries: 40 diff --git a/config/jobs-devops/global.yaml b/config/jobs-devops/global.yaml new file mode 100644 index 0000000..ff88d0c --- /dev/null +++ b/config/jobs-devops/global.yaml @@ -0,0 +1,6 @@ +--- +- defaults: + name: global + project_folder: /workspace/hw8 + test_image_tag: "1.0.0" + build_keep: 40 diff --git a/config/jobs-devops/templates/infra-health-check.yaml b/config/jobs-devops/templates/infra-health-check.yaml new file mode 100644 index 0000000..c57d29a --- /dev/null +++ b/config/jobs-devops/templates/infra-health-check.yaml @@ -0,0 +1,11 @@ +--- +- job: + name: infra-health-check + description: "Проверка инфраструктуры Jenkins/Registry/Agent образов." + project-type: pipeline + concurrent: false + sandbox: true + properties: + - build-discarder: + num-to-keep: 30 + dsl: !include-raw-verbatim: ../../jobs/scripts/infra-health-check.groovy diff --git a/config/jobs-devops/templates/jobs-uploader.yaml b/config/jobs-devops/templates/jobs-uploader.yaml new file mode 100644 index 0000000..17a61cb --- /dev/null +++ b/config/jobs-devops/templates/jobs-uploader.yaml @@ -0,0 +1,16 @@ +--- +- job: + name: jobs-uploader + description: "Обновляет Jenkins jobs из JJB YAML (config/jobs)." + project-type: pipeline + concurrent: false + sandbox: true + properties: + - build-discarder: + num-to-keep: 30 + parameters: + - string: + name: JJB_PATH + default: /workspace/hw8/config/jobs + description: "Путь до JJB-конфигов" + dsl: !include-raw-verbatim: ../../jobs/scripts/jobs-uploader.groovy diff --git a/config/jobs-devops/view.yaml b/config/jobs-devops/view.yaml new file mode 100644 index 0000000..a98ba5e --- /dev/null +++ b/config/jobs-devops/view.yaml @@ -0,0 +1,18 @@ +--- +- view: + name: DevOps + view-type: list + description: "Инфраструктурные job" + filter-executors: true + filter-queue: true + job-name: + - jobs-uploader + - infra-health-check + columns: + - status + - weather + - job + - last-success + - last-failure + - last-duration + - build-button diff --git a/config/jobs/global.yaml b/config/jobs/global.yaml index 8eb5f9e..ff88d0c 100644 --- a/config/jobs/global.yaml +++ b/config/jobs/global.yaml @@ -1,6 +1,6 @@ --- - defaults: name: global - project_folder: /workspace/otus-autotests + project_folder: /workspace/hw8 test_image_tag: "1.0.0" build_keep: 40 diff --git a/config/jobs/scripts/infra-health-check.groovy b/config/jobs/scripts/infra-health-check.groovy index 3f642e5..2868998 100644 --- a/config/jobs/scripts/infra-health-check.groovy +++ b/config/jobs/scripts/infra-health-check.groovy @@ -5,6 +5,18 @@ pipeline { ansiColor('xterm') } stages { + stage('Prepare Metadata') { + steps { + script { + wrap([$class: 'BuildUser']) { + env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system' + env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system' + } + currentBuild.displayName = "#${env.BUILD_NUMBER} infra" + currentBuild.description = "by=${env.RUN_TRIGGER_NAME}" + } + } + } stage('Check Docker & Registry') { steps { sh ''' @@ -17,13 +29,12 @@ pipeline { ''' } } - stage('Check Sources') { + stage('Check Job Configs') { steps { sh ''' set -eux - test -f /workspace/otus-autotests/homework_4/pom.xml - test -f /workspace/otus-autotests/hw7/pom.xml - test -f /workspace/otus-autotests/hw8/config/jobs/global.yaml + HW8_ROOT_PATH="${HW8_ROOT:-/workspace/hw8}" + test -f "${HW8_ROOT_PATH}/config/jobs/global.yaml" ''' } } diff --git a/config/jobs/scripts/jobs-uploader.groovy b/config/jobs/scripts/jobs-uploader.groovy index 7db1cc2..88f07a1 100644 --- a/config/jobs/scripts/jobs-uploader.groovy +++ b/config/jobs/scripts/jobs-uploader.groovy @@ -5,6 +5,18 @@ pipeline { ansiColor('xterm') } stages { + stage('Prepare Metadata') { + steps { + script { + wrap([$class: 'BuildUser']) { + env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system' + env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system' + } + currentBuild.displayName = "#${env.BUILD_NUMBER} jjb" + currentBuild.description = "by=${env.RUN_TRIGGER_NAME}; jjbPath=${params.JJB_PATH}" + } + } + } stage('Validate Input') { steps { sh ''' diff --git a/config/jobs/scripts/qa-api-citrus-tests.groovy b/config/jobs/scripts/qa-api-citrus-tests.groovy index 08e7741..e02dcfd 100644 --- a/config/jobs/scripts/qa-api-citrus-tests.groovy +++ b/config/jobs/scripts/qa-api-citrus-tests.groovy @@ -5,19 +5,47 @@ pipeline { ansiColor('xterm') } stages { + stage('Prepare Metadata') { + steps { + script { + wrap([$class: 'BuildUser']) { + env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system' + env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system' + } + currentBuild.displayName = "#${env.BUILD_NUMBER} api" + currentBuild.description = + "by=${env.RUN_TRIGGER_NAME}; repo=${params.QA_REPO_URL}; ref=${params.QA_REPO_REF}" + } + } + } stage('Run API Tests In Docker') { steps { sh ''' set -eux + rm -rf ./sources + git clone "${QA_REPO_URL}" ./sources + git -C ./sources checkout "${QA_REPO_REF}" + GIT_SHA="$(git -C ./sources rev-parse --short HEAD)" + rm -rf ./artifacts mkdir -p ./artifacts + { + echo "job=${JOB_NAME}" + echo "build=${BUILD_NUMBER}" + echo "trigger_user=${RUN_TRIGGER_USER}" + echo "trigger_name=${RUN_TRIGGER_NAME}" + echo "qa_repo_url=${QA_REPO_URL}" + echo "qa_repo_ref=${QA_REPO_REF}" + echo "qa_repo_sha=${GIT_SHA}" + echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)" + } > ./artifacts/run-info.txt CID="$(docker create localhost:5005/otus/test-api:1.0.0 bash -lc "set -e; cd /workspace; mvn -f citrus-tests/pom.xml test")" cleanup_container() { docker rm -f "${CID}" >/dev/null 2>&1 || true } trap cleanup_container EXIT INT TERM - tar -C /workspace/otus-autotests/homework_4 -cf - . | docker cp - "${CID}:/workspace" + tar -C ./sources -cf - . | docker cp - "${CID}:/workspace" set +e docker start -a "${CID}" TEST_RC=$? @@ -32,7 +60,7 @@ pipeline { post { always { junit allowEmptyResults: true, testResults: 'artifacts/citrus-target/surefire-reports/*.xml' - archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/citrus-target/**' + archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/run-info.txt,artifacts/citrus-target/**' } } } diff --git a/config/jobs/scripts/qa-mobile-appium-tests.groovy b/config/jobs/scripts/qa-mobile-appium-tests.groovy index efeb28d..7c24b91 100644 --- a/config/jobs/scripts/qa-mobile-appium-tests.groovy +++ b/config/jobs/scripts/qa-mobile-appium-tests.groovy @@ -5,13 +5,61 @@ pipeline { ansiColor('xterm') } stages { + stage('Prepare Metadata') { + steps { + script { + wrap([$class: 'BuildUser']) { + env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system' + env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system' + } + currentBuild.displayName = "#${env.BUILD_NUMBER} mobile" + currentBuild.description = + "by=${env.RUN_TRIGGER_NAME}; repo=${params.MOBILE_REPO_URL}; ref=${params.MOBILE_REPO_REF}; emulators=${params.MOBILE_MAX_EMULATORS}; junit=${params.JUNIT_PARALLELISM}; order=${params.TEST_CLASSES_ORDER}; rerun=${params.SUREFIRE_RERUN_FAILING}" + } + } + } stage('Prepare Workspace') { steps { sh ''' set -eux rm -rf ./project - mkdir -p ./project - cp -a /workspace/otus-autotests/hw7/. ./project/ + git clone "${MOBILE_REPO_URL}" ./project + git -C ./project checkout "${MOBILE_REPO_REF}" + GIT_SHA="$(git -C ./project rev-parse --short HEAD)" + if ! grep -q "allure-junit5" ./project/pom.xml; then + awk -v ver="${ALLURE_ADAPTER_VERSION}" ' + index($0, "") && !done { + print " " + print " io.qameta.allure" + print " allure-junit5" + print " " ver "" + print " test" + print " " + done=1 + } + { print } + ' ./project/pom.xml > ./project/pom.xml.tmp + mv ./project/pom.xml.tmp ./project/pom.xml + fi + mkdir -p ./project/target/allure-results + { + echo "job=${JOB_NAME}" + echo "build=${BUILD_NUMBER}" + echo "trigger_user=${RUN_TRIGGER_USER}" + echo "trigger_name=${RUN_TRIGGER_NAME}" + echo "mobile_repo_url=${MOBILE_REPO_URL}" + echo "mobile_repo_ref=${MOBILE_REPO_REF}" + echo "mobile_repo_sha=${GIT_SHA}" + echo "app_url=${APP_URL}" + echo "mobile_max_emulators=${MOBILE_MAX_EMULATORS}" + echo "junit_parallelism=${JUNIT_PARALLELISM}" + echo "test_classes_order=${TEST_CLASSES_ORDER}" + echo "surefire_rerun_failing=${SUREFIRE_RERUN_FAILING}" + echo "db_url=${DB_URL}" + echo "db_user=${DB_USER}" + echo "reservation_owner=${RESERVATION_OWNER}" + echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)" + } > ./project/run-info.txt ''' } } @@ -20,7 +68,8 @@ pipeline { dir('project') { sh ''' set -eux - COMPOSE_FILE="/workspace/otus-autotests/hw8/config/compose/mobile-ci.compose.yml" + HW8_ROOT_PATH="${HW8_ROOT:-/workspace/hw8}" + COMPOSE_FILE="${HW8_ROOT_PATH}/config/compose/mobile-ci.compose.yml" if docker compose version >/dev/null 2>&1; then compose_cmd() { PROJECT_DIR="$PWD" docker compose -f "${COMPOSE_FILE}" "$@"; } elif docker-compose version >/dev/null 2>&1; then @@ -30,11 +79,32 @@ pipeline { exit 1 fi export COMPOSE_PROJECT_NAME=mobileci - compose_cmd down -v --remove-orphans || true - compose_cmd up -d wiremock android-emulator-1 android-emulator-2 + MAX_EMULATORS="${MOBILE_MAX_EMULATORS:-1}" + if ! [ "${MAX_EMULATORS}" -eq "${MAX_EMULATORS}" ] 2>/dev/null; then + echo "Invalid MOBILE_MAX_EMULATORS='${MAX_EMULATORS}', expected integer" + exit 1 + fi + if [ "${MAX_EMULATORS}" -lt 1 ] || [ "${MAX_EMULATORS}" -gt 2 ]; then + echo "Invalid MOBILE_MAX_EMULATORS='${MAX_EMULATORS}', expected 1 or 2" + exit 1 + fi + SERVICES="wiremock android-emulator-1" + if [ "${MAX_EMULATORS}" -ge 2 ]; then + SERVICES="${SERVICES} android-emulator-2" + fi + + compose_cmd down -v --remove-orphans || true + compose_cmd up -d ${SERVICES} EMULATORS="" + SELECTED=0 for service in android-emulator-1 android-emulator-2; do + if [ "${service}" = "android-emulator-2" ] && [ "${MAX_EMULATORS}" -lt 2 ]; then + continue + fi + if [ "${SELECTED}" -ge "${MAX_EMULATORS}" ]; then + break + fi cid="$(compose_cmd ps -q "$service")" if [ -z "${cid}" ]; then echo "Container for ${service} not found, skipping" @@ -55,6 +125,7 @@ pipeline { EMULATORS="${EMULATORS}," fi EMULATORS="${EMULATORS}${service}|http://${service}:4723|Android Emulator|${APP_URL}" + SELECTED=$((SELECTED + 1)) else echo "Service ${service} is not healthy in time, excluding from test run" docker logs "${cid}" || true @@ -76,40 +147,258 @@ pipeline { dir('project') { sh ''' set -eux - rm -rf ./target + rm -rf ./target ./target-run MOBILE_EMULATORS_VALUE="$(cat ./.mobile_emulators.txt)" + EMULATOR_ENTRIES_COUNT="$(printf '%s' "${MOBILE_EMULATORS_VALUE}" | awk -F, '{print NF}')" + DOCKER_NETWORK_ARG="--network mobileci_default" + if [ "${EMULATOR_ENTRIES_COUNT}" -eq 1 ]; then + SINGLE_SERVICE="$(printf '%s' "${MOBILE_EMULATORS_VALUE}" | cut -d'|' -f1)" + if [ "${SINGLE_SERVICE}" = "android-emulator-1" ] || [ "${SINGLE_SERVICE}" = "android-emulator-2" ]; then + SINGLE_EMULATOR_CID="$(docker ps -q --filter "name=mobileci-${SINGLE_SERVICE}" | head -n1 || true)" + if [ -n "${SINGLE_EMULATOR_CID}" ]; then + DOCKER_NETWORK_ARG="--network container:${SINGLE_EMULATOR_CID}" + MOBILE_EMULATORS_VALUE="$(printf '%s' "${MOBILE_EMULATORS_VALUE}" | awk -F'|' 'BEGIN { OFS="|" } { $2="http://127.0.0.1:4723"; print $1, $2, $3, $4 }')" + fi + fi + fi DB_PASSWORD_EFFECTIVE="${DB_PASSWORD:-${MOBILE_DB_PASSWORD:-}}" if [ -z "${DB_PASSWORD_EFFECTIVE}" ]; then echo "DB password is not set. Provide DB_PASSWORD job parameter or MOBILE_DB_PASSWORD env variable." exit 1 fi - CID="$(docker create \ - --network mobileci_default \ - -e DB_URL="${DB_URL:-}" \ - -e DB_USER="${DB_USER:-}" \ - -e DB_PASSWORD="${DB_PASSWORD_EFFECTIVE}" \ - -e APP_URL="${APP_URL:-}" \ + + # Validate Appium endpoints from the same Docker network as test container. + if ! docker run --rm \ + ${DOCKER_NETWORK_ARG} \ -e MOBILE_EMULATORS="${MOBILE_EMULATORS_VALUE}" \ - -e WISHLISTS_USERNAME="${WISHLISTS_USERNAME:-}" \ - -e WISHLISTS_PASSWORD="${WISHLISTS_PASSWORD:-}" \ - -e GIFTS_USERNAME="${GIFTS_USERNAME:-}" \ - -e GIFTS_PASSWORD="${GIFTS_PASSWORD:-}" \ - -e RESERVATION_USERNAME="${RESERVATION_USERNAME:-}" \ - -e RESERVATION_PASSWORD="${RESERVATION_PASSWORD:-}" \ - -e RESERVATION_OWNER="${RESERVATION_OWNER:-}" \ localhost:5005/otus/test-mobile:1.0.0 \ - bash -lc "set -e; cd /workspace; timeout 1800s mvn -Dallure.results.directory=target/allure-results test")" - cleanup_container() { + bash -lc ' + set -euo pipefail + IFS="," read -ra TARGETS <<< "${MOBILE_EMULATORS}" + for target in "${TARGETS[@]}"; do + endpoint="$(echo "${target}" | cut -d"|" -f2)" + if [ -z "${endpoint}" ]; then + echo "Invalid MOBILE_EMULATORS entry: ${target}" + exit 1 + fi + ready="false" + for i in $(seq 1 60); do + if curl -fsS "${endpoint}/status" >/dev/null 2>&1; then + ready="true" + break + fi + sleep 2 + done + if [ "${ready}" != "true" ]; then + echo "Appium endpoint is not reachable from test container: ${endpoint}" + exit 1 + fi + done + '; then + echo "Appium preflight failed. Dumping emulator logs." + for service in android-emulator-1 android-emulator-2; do + cid="$(docker ps -q --filter "name=mobileci-${service}" | head -n1 || true)" + if [ -n "${cid}" ]; then + echo "--- logs for ${service} (${cid}) ---" + docker logs --tail 200 "${cid}" || true + fi + done + exit 1 + fi + + run_single_class() { + class_name="$1" + attempt="$2" + class_key="$(echo "${class_name}" | sed 's/[^a-zA-Z0-9._-]/_/g')" + attempt_dir="./target-run/${class_key}/attempt-${attempt}" + mkdir -p "${attempt_dir}" + + CID="$(docker create \ + ${DOCKER_NETWORK_ARG} \ + -e DB_URL="${DB_URL:-}" \ + -e DB_USER="${DB_USER:-}" \ + -e DB_PASSWORD="${DB_PASSWORD_EFFECTIVE}" \ + -e APP_URL="${APP_URL:-}" \ + -e MOBILE_EMULATORS="${MOBILE_EMULATORS_VALUE}" \ + -e WISHLISTS_USERNAME="${WISHLISTS_USERNAME:-}" \ + -e WISHLISTS_PASSWORD="${WISHLISTS_PASSWORD:-}" \ + -e GIFTS_USERNAME="${GIFTS_USERNAME:-}" \ + -e GIFTS_PASSWORD="${GIFTS_PASSWORD:-}" \ + -e RESERVATION_USERNAME="${RESERVATION_USERNAME:-}" \ + -e RESERVATION_PASSWORD="${RESERVATION_PASSWORD:-}" \ + -e RESERVATION_OWNER="${RESERVATION_OWNER:-}" \ + localhost:5005/otus/test-mobile:1.0.0 \ + bash -lc "set -e; cd /workspace; timeout 1800s mvn -Dallure.version=${ALLURE_ADAPTER_VERSION} -Djunit.jupiter.execution.parallel.enabled=false -Djunit.jupiter.execution.parallel.config.fixed.parallelism=${JUNIT_PARALLELISM:-1} -Dtest=${class_name} -Dsurefire.rerunFailingTestsCount=${SUREFIRE_RERUN_FAILING:-0} -Dallure.results.directory=target/allure-results test")" + + tar -C "$PWD" -cf - . | docker cp - "${CID}:/workspace" + set +e + docker start -a "${CID}" + class_rc=$? + set -e + docker cp "${CID}:/workspace/target" "${attempt_dir}/target" || true docker rm -f "${CID}" >/dev/null 2>&1 || true + return "${class_rc}" } - trap cleanup_container EXIT INT TERM - tar -C "$PWD" -cf - . | docker cp - "${CID}:/workspace" - set +e - docker start -a "${CID}" - TEST_RC=$? - docker cp "${CID}:/workspace/target" "./target" || true - trap - EXIT INT TERM - docker rm -f "${CID}" || true + + collect_attempt_artifacts() { + src_dir="$1" + mkdir -p ./target/allure-results ./target/surefire-reports + if [ -d "${src_dir}/allure-results" ]; then + cp -a "${src_dir}/allure-results/." ./target/allure-results/ || true + fi + if [ -d "${src_dir}/surefire-reports" ]; then + cp -a "${src_dir}/surefire-reports/." ./target/surefire-reports/ || true + fi + } + + if [ -n "${TEST_CLASSES_ORDER:-}" ]; then + printf '%s\n' "${TEST_CLASSES_ORDER}" | tr ',' '\n' > .test-classes.txt + else + cat > .test-classes.txt <<'EOF' +ru.otus.mobile.tests.WishlistsTest +ru.otus.mobile.tests.GiftsTest +ru.otus.mobile.tests.ReservationTest +EOF + fi + + TEST_RC=0 + MAX_CLASS_ATTEMPTS=2 + while IFS= read -r raw_class; do + class_name="$(echo "${raw_class}" | xargs)" + if [ -z "${class_name}" ]; then + continue + fi + CLASS_OK=0 + LAST_ATTEMPT_DIR="" + for attempt in $(seq 1 "${MAX_CLASS_ATTEMPTS}"); do + echo "Running ${class_name} (attempt ${attempt}/${MAX_CLASS_ATTEMPTS})" + if run_single_class "${class_name}" "${attempt}"; then + CLASS_OK=1 + LAST_ATTEMPT_DIR="./target-run/$(echo "${class_name}" | sed 's/[^a-zA-Z0-9._-]/_/g')/attempt-${attempt}/target" + collect_attempt_artifacts "${LAST_ATTEMPT_DIR}" + break + fi + LAST_ATTEMPT_DIR="./target-run/$(echo "${class_name}" | sed 's/[^a-zA-Z0-9._-]/_/g')/attempt-${attempt}/target" + sleep 3 + done + + if [ "${CLASS_OK}" -ne 1 ]; then + TEST_RC=1 + if [ -n "${LAST_ATTEMPT_DIR}" ] && [ -d "${LAST_ATTEMPT_DIR}" ]; then + collect_attempt_artifacts "${LAST_ATTEMPT_DIR}" + fi + fi + done < .test-classes.txt + + mkdir -p ./target/mobile-debug + docker network inspect mobileci_default > ./target/mobile-debug/network.inspect.json 2>&1 || true + for service in android-emulator-1 android-emulator-2; do + emu_cid="$(docker ps -q --filter "name=mobileci-${service}" | head -n1 || true)" + if [ -n "${emu_cid}" ]; then + docker inspect "${emu_cid}" > "./target/mobile-debug/${service}.inspect.json" 2>&1 || true + docker logs "${emu_cid}" > "./target/mobile-debug/${service}.log" 2>&1 || true + fi + done + GIT_SHA="$(git -C "$PWD" rev-parse --short HEAD)" + mkdir -p ./target/allure-results + + SCREENSHOT_FILES="$(find ./target ./target-run -type f \\( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \\) 2>/dev/null || true)" + ATTACHMENT_COUNT=0 + ATTACHMENTS_JSON="" + if [ -n "${SCREENSHOT_FILES}" ]; then + while IFS= read -r screenshot_file; do + if [ -z "${screenshot_file}" ] || [ ! -f "${screenshot_file}" ]; then + continue + fi + ATTACHMENT_COUNT=$((ATTACHMENT_COUNT + 1)) + ext="$(echo "${screenshot_file}" | awk -F. '{print tolower($NF)}')" + if [ "${ext}" = "jpg" ] || [ "${ext}" = "jpeg" ]; then + mime="image/jpeg" + else + ext="png" + mime="image/png" + fi + source_name="external-screenshot-${ATTACHMENT_COUNT}.${ext}" + cp "${screenshot_file}" "./target/allure-results/${source_name}" || continue + safe_name="$(basename "${screenshot_file}" | sed 's/"/\\"/g')" + if [ -n "${ATTACHMENTS_JSON}" ]; then + ATTACHMENTS_JSON="${ATTACHMENTS_JSON}," + fi + ATTACHMENTS_JSON="${ATTACHMENTS_JSON}{\"name\":\"${safe_name}\",\"type\":\"${mime}\",\"source\":\"${source_name}\"}" + done < "./target/allure-results/${EXTRA_UUID}-result.json" < ./target/allure-results/environment.properties + cat > ./target/allure-results/executor.json < ./target/allure-results/categories.json <<'EOF' +[ + { + "name": "Infrastructure issues", + "matchedStatuses": ["broken"], + "messageRegex": ".*(Connection refused|No route to host|timed out|timeout|No healthy emulators available).*" + }, + { + "name": "Mobile UI locator/assertion issues", + "matchedStatuses": ["failed", "broken"], + "messageRegex": ".*(Element not found|NoSuchElement|AssertionError).*" + } +] +EOF exit "${TEST_RC}" ''' } @@ -121,7 +410,8 @@ pipeline { dir('project') { sh ''' set +e - COMPOSE_FILE="/workspace/otus-autotests/hw8/config/compose/mobile-ci.compose.yml" + HW8_ROOT_PATH="${HW8_ROOT:-/workspace/hw8}" + COMPOSE_FILE="${HW8_ROOT_PATH}/config/compose/mobile-ci.compose.yml" if docker compose version >/dev/null 2>&1; then compose_cmd() { PROJECT_DIR="$PWD" docker compose -f "${COMPOSE_FILE}" "$@"; } elif docker-compose version >/dev/null 2>&1; then @@ -142,7 +432,7 @@ pipeline { } } junit allowEmptyResults: true, testResults: 'project/target/surefire-reports/*.xml' - archiveArtifacts allowEmptyArchive: true, artifacts: 'project/target/**' + archiveArtifacts allowEmptyArchive: true, artifacts: 'project/run-info.txt,project/target/**' } } } diff --git a/config/jobs/scripts/qa-runner.groovy b/config/jobs/scripts/qa-runner.groovy index 2558f17..2273307 100644 --- a/config/jobs/scripts/qa-runner.groovy +++ b/config/jobs/scripts/qa-runner.groovy @@ -5,29 +5,48 @@ pipeline { ansiColor('xterm') } stages { + stage('Prepare Metadata') { + steps { + script { + wrap([$class: 'BuildUser']) { + env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system' + env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system' + } + currentBuild.displayName = "#${env.BUILD_NUMBER} runner" + currentBuild.description = "by=${env.RUN_TRIGGER_NAME}; qaRef=${params.QA_REPO_REF}; mobileRef=${params.MOBILE_REPO_REF}" + } + } + } stage('Run Jobs In Parallel') { steps { script { def fanout = [:] fanout['selenium'] = { - build job: 'qa-selenium-tests', + def run = build job: 'qa-selenium-tests', wait: true, - propagate: true, + propagate: false, parameters: [ + string(name: 'QA_REPO_URL', value: params.QA_REPO_URL), + string(name: 'QA_REPO_REF', value: params.QA_REPO_REF), string(name: 'BROWSER', value: params.BROWSER), string(name: 'BASE_URL', value: params.BASE_URL), string(name: 'EXECUTION_MODE', value: params.EXECUTION_MODE), string(name: 'SELENOID_URL', value: params.SELENOID_URL), string(name: 'HEADLESS', value: params.HEADLESS) ] + writeFile file: 'downstream-selenium.txt', text: "${run.number}|${run.result}\n" } fanout['mobile'] = { - build job: 'qa-mobile-appium-tests', + def run = build job: 'qa-mobile-appium-tests', wait: true, - propagate: true, + propagate: false, parameters: [ + string(name: 'MOBILE_REPO_URL', value: params.MOBILE_REPO_URL), + string(name: 'MOBILE_REPO_REF', value: params.MOBILE_REPO_REF), + string(name: 'MOBILE_MAX_EMULATORS', value: params.MOBILE_MAX_EMULATORS), + string(name: 'JUNIT_PARALLELISM', value: params.JUNIT_PARALLELISM), string(name: 'APP_URL', value: params.APP_URL), string(name: 'DB_URL', value: params.DB_URL), string(name: 'DB_USER', value: params.DB_USER), @@ -40,13 +59,64 @@ pipeline { string(name: 'RESERVATION_PASSWORD', value: params.RESERVATION_PASSWORD), string(name: 'RESERVATION_OWNER', value: params.RESERVATION_OWNER) ] + writeFile file: 'downstream-mobile.txt', text: "${run.number}|${run.result}\n" } fanout['api'] = { - build job: 'qa-api-citrus-tests', wait: true, propagate: true + def run = build job: 'qa-api-citrus-tests', + wait: true, + propagate: false, + parameters: [ + string(name: 'QA_REPO_URL', value: params.QA_REPO_URL), + string(name: 'QA_REPO_REF', value: params.QA_REPO_REF) + ] + writeFile file: 'downstream-api.txt', text: "${run.number}|${run.result}\n" } parallel fanout + + def parseRun = { String fileName -> + if (!fileExists(fileName)) { + return [number: 'n/a', result: 'NOT_BUILT'] + } + def parts = readFile(fileName).trim().tokenize('|') + return [ + number: parts ? parts[0] : 'n/a', + result: parts.size() > 1 ? parts[1] : 'UNKNOWN' + ] + } + + def seleniumRun = parseRun('downstream-selenium.txt') + def mobileRun = parseRun('downstream-mobile.txt') + def apiRun = parseRun('downstream-api.txt') + def lines = [] + lines << "job=${env.JOB_NAME}" + lines << "build=${env.BUILD_NUMBER}" + lines << "trigger_user=${env.RUN_TRIGGER_USER}" + lines << "trigger_name=${env.RUN_TRIGGER_NAME}" + lines << "qa_repo_url=${params.QA_REPO_URL}" + lines << "qa_repo_ref=${params.QA_REPO_REF}" + lines << "mobile_repo_url=${params.MOBILE_REPO_URL}" + lines << "mobile_repo_ref=${params.MOBILE_REPO_REF}" + lines << "selenium_build=${seleniumRun.number}" + lines << "selenium_result=${seleniumRun.result}" + lines << "mobile_build=${mobileRun.number}" + lines << "mobile_result=${mobileRun.result}" + lines << "api_build=${apiRun.number}" + lines << "api_result=${apiRun.result}" + lines << "selenium_link=/job/qa-selenium-tests/${seleniumRun.number}/" + lines << "mobile_link=/job/qa-mobile-appium-tests/${mobileRun.number}/" + lines << "api_link=/job/qa-api-citrus-tests/${apiRun.number}/" + lines << "selenium_link_abs=${env.JENKINS_URL}job/qa-selenium-tests/${seleniumRun.number}/" + lines << "mobile_link_abs=${env.JENKINS_URL}job/qa-mobile-appium-tests/${mobileRun.number}/" + lines << "api_link_abs=${env.JENKINS_URL}job/qa-api-citrus-tests/${apiRun.number}/" + writeFile file: 'runner-summary.txt', text: lines.join('\n') + '\n' + archiveArtifacts allowEmptyArchive: false, artifacts: 'runner-summary.txt' + + def failed = [seleniumRun, mobileRun, apiRun].any { it.result != 'SUCCESS' } + if (failed) { + error("One or more downstream QA jobs failed. See runner-summary.txt for build links and statuses.") + } } } } diff --git a/config/jobs/scripts/qa-selenium-tests.groovy b/config/jobs/scripts/qa-selenium-tests.groovy index c859b71..13619fc 100644 --- a/config/jobs/scripts/qa-selenium-tests.groovy +++ b/config/jobs/scripts/qa-selenium-tests.groovy @@ -5,12 +5,60 @@ pipeline { ansiColor('xterm') } stages { + stage('Prepare Metadata') { + steps { + script { + wrap([$class: 'BuildUser']) { + env.RUN_TRIGGER_USER = env.BUILD_USER_ID ?: env.BUILD_USER ?: 'system' + env.RUN_TRIGGER_NAME = env.BUILD_USER ?: env.BUILD_USER_ID ?: 'system' + } + currentBuild.displayName = "#${env.BUILD_NUMBER} ${params.BROWSER}/${params.HEADLESS}" + currentBuild.description = + "by=${env.RUN_TRIGGER_NAME}; repo=${params.QA_REPO_URL}; ref=${params.QA_REPO_REF}; mode=${params.EXECUTION_MODE}" + } + } + } stage('Run Selenium Tests In Docker') { steps { sh ''' set -eux + rm -rf ./sources + git clone "${QA_REPO_URL}" ./sources + git -C ./sources checkout "${QA_REPO_REF}" + GIT_SHA="$(git -C ./sources rev-parse --short HEAD)" + if ! grep -q "allure-junit5" ./sources/pom.xml; then + awk -v ver="${ALLURE_ADAPTER_VERSION}" ' + index($0, "") && !done { + print " " + print " io.qameta.allure" + print " allure-junit5" + print " " ver "" + print " test" + print " " + done=1 + } + { print } + ' ./sources/pom.xml > ./sources/pom.xml.tmp + mv ./sources/pom.xml.tmp ./sources/pom.xml + fi + rm -rf ./artifacts mkdir -p ./artifacts + { + echo "job=${JOB_NAME}" + echo "build=${BUILD_NUMBER}" + echo "trigger_user=${RUN_TRIGGER_USER}" + echo "trigger_name=${RUN_TRIGGER_NAME}" + echo "qa_repo_url=${QA_REPO_URL}" + echo "qa_repo_ref=${QA_REPO_REF}" + echo "qa_repo_sha=${GIT_SHA}" + echo "browser=${BROWSER}" + echo "headless=${HEADLESS}" + echo "execution_mode=${EXECUTION_MODE}" + echo "selenoid_url=${SELENOID_URL}" + echo "base_url=${BASE_URL}" + echo "timestamp_utc=$(date -u +%Y-%m-%dT%H:%M:%SZ)" + } > ./artifacts/run-info.txt EXTRA_ARGS="" if [ "${BROWSER}" = "chrome" ]; then @@ -20,16 +68,55 @@ pipeline { CID="$(docker create \ --add-host=host.docker.internal:host-gateway \ localhost:5005/otus/test-selenium:1.0.0 \ - bash -lc "set -e; cd /workspace; mvn -Dexecution.mode=${EXECUTION_MODE} -Dbrowser=${BROWSER} -Dbrowser.version= -Dselenoid.url=${SELENOID_URL} -Dbase.url=${BASE_URL} -Dselenide.headless=${HEADLESS} -Dallure.results.directory=target/allure-results ${EXTRA_ARGS} test")" + bash -lc "set -e; cd /workspace; mvn -Dallure.version=${ALLURE_ADAPTER_VERSION} -Dexecution.mode=${EXECUTION_MODE} -Dbrowser=${BROWSER} -Dbrowser.version= -Dselenoid.url=${SELENOID_URL} -Dbase.url=${BASE_URL} -Dselenide.headless=${HEADLESS} -Dallure.results.directory=target/allure-results ${EXTRA_ARGS} test")" cleanup_container() { docker rm -f "${CID}" >/dev/null 2>&1 || true } trap cleanup_container EXIT INT TERM - tar -C /workspace/otus-autotests/homework_4 -cf - . | docker cp - "${CID}:/workspace" + tar -C ./sources -cf - . | docker cp - "${CID}:/workspace" set +e docker start -a "${CID}" TEST_RC=$? - docker cp "${CID}:/workspace/target" "./artifacts/target" || true + docker cp "${CID}:/workspace/target" "./artifacts" || true + mkdir -p ./artifacts/target/allure-results + { + echo "job=${JOB_NAME}" + echo "build=${BUILD_NUMBER}" + echo "trigger.user=${RUN_TRIGGER_USER}" + echo "trigger.name=${RUN_TRIGGER_NAME}" + echo "repo.url=${QA_REPO_URL}" + echo "repo.ref=${QA_REPO_REF}" + echo "repo.sha=${GIT_SHA}" + echo "browser=${BROWSER}" + echo "headless=${HEADLESS}" + echo "execution.mode=${EXECUTION_MODE}" + echo "base.url=${BASE_URL}" + } > ./artifacts/target/allure-results/environment.properties + cat > ./artifacts/target/allure-results/executor.json < ./artifacts/target/allure-results/categories.json <<'EOF' +[ + { + "name": "Infrastructure issues", + "matchedStatuses": ["broken"], + "messageRegex": ".*(Connection refused|No route to host|timed out|timeout).*" + }, + { + "name": "UI locator/assertion issues", + "matchedStatuses": ["failed", "broken"], + "messageRegex": ".*(Element not found|NoSuchElement|AssertionError).*" + } +] +EOF trap - EXIT INT TERM docker rm -f "${CID}" || true exit "${TEST_RC}" @@ -47,7 +134,7 @@ pipeline { } } junit allowEmptyResults: true, testResults: 'artifacts/target/surefire-reports/*.xml' - archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/target/**' + archiveArtifacts allowEmptyArchive: true, artifacts: 'artifacts/run-info.txt,artifacts/target/**' } } } diff --git a/config/jobs/templates/jobs-uploader.yaml b/config/jobs/templates/jobs-uploader.yaml index 197e680..6c94bbf 100644 --- a/config/jobs/templates/jobs-uploader.yaml +++ b/config/jobs/templates/jobs-uploader.yaml @@ -11,6 +11,6 @@ parameters: - string: name: JJB_PATH - default: /workspace/otus-autotests/hw8/config/jobs + default: /workspace/hw8/config/jobs description: "Путь до JJB-конфигов" dsl: !include-raw-verbatim: ../scripts/jobs-uploader.groovy diff --git a/config/jobs/templates/qa-api-citrus-tests.yaml b/config/jobs/templates/qa-api-citrus-tests.yaml index 0e04bf4..bfede70 100644 --- a/config/jobs/templates/qa-api-citrus-tests.yaml +++ b/config/jobs/templates/qa-api-citrus-tests.yaml @@ -8,4 +8,13 @@ properties: - build-discarder: num-to-keep: 50 + parameters: + - string: + name: QA_REPO_URL + default: https://git.kovbasa.ru/otus-autotests/homework_4.git + description: "Git URL репозитория с Selenium/API тестами" + - string: + name: QA_REPO_REF + default: master + description: "Git branch/tag/commit для checkout" dsl: !include-raw-verbatim: ../scripts/qa-api-citrus-tests.groovy diff --git a/config/jobs/templates/qa-mobile-appium-tests.yaml b/config/jobs/templates/qa-mobile-appium-tests.yaml index c2eacb4..b5666c1 100644 --- a/config/jobs/templates/qa-mobile-appium-tests.yaml +++ b/config/jobs/templates/qa-mobile-appium-tests.yaml @@ -9,6 +9,38 @@ - build-discarder: num-to-keep: 30 parameters: + - string: + name: MOBILE_REPO_URL + default: https://git.kovbasa.ru/otus-autotests/homework_7.git + description: "Git URL репозитория с Appium тестами" + - string: + name: MOBILE_REPO_REF + default: master + description: "Git branch/tag/commit для checkout" + - string: + name: ALLURE_ADAPTER_VERSION + default: 2.29.1 + description: "Версия allure-junit5 адаптера" + - choice: + name: MOBILE_MAX_EMULATORS + choices: + - "1" + - "2" + description: "Сколько эмуляторов использовать в прогоне (1=стабильнее, 2=быстрее)" + - choice: + name: JUNIT_PARALLELISM + choices: + - "1" + - "2" + description: "Параллельность JUnit классов в мобильных тестах" + - string: + name: TEST_CLASSES_ORDER + default: "ru.otus.mobile.tests.WishlistsTest,ru.otus.mobile.tests.GiftsTest,ru.otus.mobile.tests.ReservationTest" + description: "Порядок классов для поочередного запуска (-Dtest)" + - string: + name: SUREFIRE_RERUN_FAILING + default: "0" + description: "Количество surefire rerun внутри одного запуска класса (обычно 0, т.к. ретрай делается на уровне pipeline)" - string: name: APP_URL default: http://wiremock:8080/wishlist.apk diff --git a/config/jobs/templates/qa-runner.yaml b/config/jobs/templates/qa-runner.yaml index 1ae725d..3886041 100644 --- a/config/jobs/templates/qa-runner.yaml +++ b/config/jobs/templates/qa-runner.yaml @@ -9,6 +9,28 @@ - build-discarder: num-to-keep: 50 parameters: + - string: + name: QA_REPO_URL + default: https://git.kovbasa.ru/otus-autotests/homework_4.git + - string: + name: QA_REPO_REF + default: master + - string: + name: MOBILE_REPO_URL + default: https://git.kovbasa.ru/otus-autotests/homework_7.git + - string: + name: MOBILE_REPO_REF + default: master + - choice: + name: MOBILE_MAX_EMULATORS + choices: + - "1" + - "2" + - choice: + name: JUNIT_PARALLELISM + choices: + - "1" + - "2" - choice: name: BROWSER choices: @@ -19,7 +41,7 @@ default: https://otus.ru - string: name: EXECUTION_MODE - default: selenoid + default: local - string: name: SELENOID_URL default: http://host.docker.internal:4444/wd/hub diff --git a/config/jobs/templates/qa-selenium-tests.yaml b/config/jobs/templates/qa-selenium-tests.yaml index 336064f..2dbcbd4 100644 --- a/config/jobs/templates/qa-selenium-tests.yaml +++ b/config/jobs/templates/qa-selenium-tests.yaml @@ -9,6 +9,18 @@ - build-discarder: num-to-keep: 50 parameters: + - string: + name: QA_REPO_URL + default: https://git.kovbasa.ru/otus-autotests/homework_4.git + description: "Git URL репозитория с Selenium/API тестами" + - string: + name: QA_REPO_REF + default: master + description: "Git branch/tag/commit для checkout" + - string: + name: ALLURE_ADAPTER_VERSION + default: 2.29.1 + description: "Версия allure-junit5 адаптера" - choice: name: BROWSER choices: @@ -21,7 +33,7 @@ description: "Базовый URL тестируемого сайта" - string: name: EXECUTION_MODE - default: selenoid + default: local description: "Режим запуска (local|selenoid)" - string: name: SELENOID_URL diff --git a/config/jobs/view.yaml b/config/jobs/view.yaml index fe4d6ab..10ec713 100644 --- a/config/jobs/view.yaml +++ b/config/jobs/view.yaml @@ -18,13 +18,29 @@ - build-button - view: - name: QA + name: QA-Runner view-type: list - description: "Тестовые job и раннер" + description: "Точка входа для запуска всех тестов" filter-executors: true filter-queue: true job-name: - qa-runner + columns: + - status + - weather + - job + - last-success + - last-failure + - last-duration + - build-button + +- view: + name: QA-Tests + view-type: list + description: "Тестовые job" + filter-executors: true + filter-queue: true + job-name: - qa-selenium-tests - qa-api-citrus-tests - qa-mobile-appium-tests