Files
homework_8/config/jobs/scripts/qa-mobile-appium-tests.groovy

439 lines
18 KiB
Groovy

pipeline {
agent { label 'maven' }
options {
timestamps()
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
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 "<artifactId>allure-junit5</artifactId>" ./project/pom.xml; then
awk -v ver="${ALLURE_ADAPTER_VERSION}" '
index($0, "</dependencies>") && !done {
print " <dependency>"
print " <groupId>io.qameta.allure</groupId>"
print " <artifactId>allure-junit5</artifactId>"
print " <version>" ver "</version>"
print " <scope>test</scope>"
print " </dependency>"
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
'''
}
}
stage('Start Mobile Environment') {
steps {
dir('project') {
sh '''
set -eux
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
compose_cmd() { PROJECT_DIR="$PWD" docker-compose -f "${COMPOSE_FILE}" "$@"; }
else
echo "Neither docker compose plugin nor docker-compose binary is available"
exit 1
fi
export COMPOSE_PROJECT_NAME=mobileci
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"
continue
fi
healthy="false"
for i in $(seq 1 80); do
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}starting{{end}}' "${cid}" || true)"
if [ "${status}" = "healthy" ]; then
healthy="true"
break
fi
sleep 10
done
if [ "${healthy}" = "true" ]; then
if [ -n "${EMULATORS}" ]; then
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
fi
done
if [ -z "${EMULATORS}" ]; then
echo "No healthy emulators available"
exit 1
fi
printf '%s' "${EMULATORS}" > .mobile_emulators.txt
'''
}
}
}
stage('Run Appium Tests In Docker') {
steps {
dir('project') {
sh '''
set -eux
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
# Validate Appium endpoints from the same Docker network as test container.
if ! docker run --rm \
${DOCKER_NETWORK_ARG} \
-e MOBILE_EMULATORS="${MOBILE_EMULATORS_VALUE}" \
localhost:5005/otus/test-mobile:1.0.0 \
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}"
}
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 <<EOF
${SCREENSHOT_FILES}
EOF
fi
if [ "${ATTACHMENT_COUNT}" -gt 0 ]; then
EXTRA_UUID="$(cat /proc/sys/kernel/random/uuid)"
TS_MS="$(( $(date +%s) * 1000 ))"
cat > "./target/allure-results/${EXTRA_UUID}-result.json" <<EOF
{
"uuid": "${EXTRA_UUID}",
"historyId": "external-screenshots-${BUILD_NUMBER}",
"name": "Collected screenshots",
"fullName": "pipeline.CollectedScreenshots",
"status": "passed",
"stage": "finished",
"start": ${TS_MS},
"stop": ${TS_MS},
"labels": [
{"name": "suite", "value": "Pipeline Artifacts"},
{"name": "package", "value": "pipeline"},
{"name": "testClass", "value": "CollectedScreenshots"},
{"name": "testMethod", "value": "attach"},
{"name": "framework", "value": "jenkins-pipeline"}
],
"attachments": [${ATTACHMENTS_JSON}],
"steps": [],
"parameters": []
}
EOF
fi
{
echo "job=${JOB_NAME}"
echo "build=${BUILD_NUMBER}"
echo "trigger.user=${RUN_TRIGGER_USER}"
echo "trigger.name=${RUN_TRIGGER_NAME}"
echo "repo.url=${MOBILE_REPO_URL}"
echo "repo.ref=${MOBILE_REPO_REF}"
echo "repo.sha=${GIT_SHA}"
echo "app.url=${APP_URL}"
echo "db.url=${DB_URL}"
echo "db.user=${DB_USER}"
echo "reservation.owner=${RESERVATION_OWNER}"
echo "mobile.emulators=${MOBILE_EMULATORS_VALUE}"
echo "mobile.max.emulators=${MOBILE_MAX_EMULATORS:-1}"
echo "junit.parallelism=${JUNIT_PARALLELISM:-1}"
echo "test.classes.order=${TEST_CLASSES_ORDER}"
echo "surefire.rerunFailing=${SUREFIRE_RERUN_FAILING:-1}"
} > ./target/allure-results/environment.properties
cat > ./target/allure-results/executor.json <<EOF
{
"name": "Jenkins",
"type": "jenkins",
"url": "${JENKINS_URL}",
"buildName": "${JOB_NAME} #${BUILD_NUMBER}",
"buildUrl": "${BUILD_URL}",
"reportUrl": "${BUILD_URL}allure",
"buildOrder": ${BUILD_NUMBER}
}
EOF
cat > ./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}"
'''
}
}
}
}
post {
always {
dir('project') {
sh '''
set +e
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
compose_cmd() { PROJECT_DIR="$PWD" docker-compose -f "${COMPOSE_FILE}" "$@"; }
else
echo "compose command is not available for cleanup"
exit 0
fi
export COMPOSE_PROJECT_NAME=mobileci
compose_cmd down -v --remove-orphans || true
'''
}
script {
try {
allure commandline: 'allure', includeProperties: false, jdk: '', results: [[path: 'project/target/allure-results']]
} catch (Exception ex) {
echo "Allure publisher unavailable: ${ex.message}"
}
}
junit allowEmptyResults: true, testResults: 'project/target/surefire-reports/*.xml'
archiveArtifacts allowEmptyArchive: true, artifacts: 'project/run-info.txt,project/target/**'
}
}
}