439 lines
18 KiB
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/**'
|
|
}
|
|
}
|
|
}
|