Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f3e089d
Initial pytest test migration
jvpasinatto Sep 23, 2025
7bdf097
Merge branch 'main' into pytest-complete
jvpasinatto Sep 23, 2025
fb96a0c
Refactor to adress comments
jvpasinatto Sep 24, 2025
1fc136f
Merge branch 'main' into pytest-complete
jvpasinatto Sep 24, 2025
37aa18d
Make wait more robust
jvpasinatto Sep 25, 2025
c784bf0
Add liveness test
jvpasinatto Sep 26, 2025
4b4bf43
Merge branch 'main' into pytest-complete
gkech Jan 23, 2026
cafa5f4
Add python rules in makefile
jvpasinatto Jan 23, 2026
ab14a77
update dependencies
jvpasinatto Jan 23, 2026
8fabdbe
Add more type hints
jvpasinatto Jan 23, 2026
9195e8a
Print env vars in test initialization and more
jvpasinatto Jan 23, 2026
b572b0d
Add resources collection on failure
jvpasinatto Jan 23, 2026
44858f8
move python scripts to folder
jvpasinatto Jan 29, 2026
ad7521d
fix liveness test and add more type hints
jvpasinatto Jan 29, 2026
9adffbc
fix init deploy test
jvpasinatto Jan 29, 2026
70d4f06
add py-fmt rule
jvpasinatto Jan 29, 2026
974c0d1
Update test readme and small fixes
jvpasinatto Jan 30, 2026
11eed52
use rich to handle logging
jvpasinatto Jan 30, 2026
8a100f3
Add bash wrapper
jvpasinatto Jan 30, 2026
ad60319
update lock file
jvpasinatto Jan 30, 2026
ba739c0
fix openshift detection
jvpasinatto Jan 30, 2026
a8cd1d4
divide tools into separated files plus improvements
jvpasinatto Feb 2, 2026
079499c
Fix report generation
jvpasinatto Feb 3, 2026
9a7560b
Merge branch 'main' into pytest-complete
jvpasinatto Feb 4, 2026
76c1436
Merge branch 'main' into pytest-complete
jvpasinatto May 5, 2026
b5c5903
remove test retry and add permission in gh action py-check
jvpasinatto May 5, 2026
c6c23d3
Merge branch 'main' into pytest-complete
egegunes Jun 4, 2026
fb8cc71
Merge branch 'main' into pytest-complete
gkech Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/e2e-py-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: e2e-tests Python Quality Check

on:
pull_request:
paths:
- 'e2e-tests/**/*.py'

Comment on lines +3 to +7
permissions:
contents: read

jobs:
quality-check:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version-file: "pyproject.toml"

- name: Install dependencies
run: uv sync --locked

- name: Run ruff check
run: uv run ruff check e2e-tests/

- name: Run mypy
run: uv run mypy e2e-tests/
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,7 @@ bin/
projects/
installers/olm/operator_*.yaml
installers/olm/bundles

# Test Reports
e2e-tests/reports/
e2e-tests/**/__pycache__/
182 changes: 115 additions & 67 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ void pushLogFile(String FILE_NAME) {
}
}

void pushReportFile() {
echo "Push final_report.html to S3!"
withCredentials([aws(credentialsId: 'AMI/OVF', accessKeyVariable: 'AWS_ACCESS_KEY_ID', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY')]) {
sh """
S3_PATH=s3://percona-jenkins-artifactory-public/\$JOB_NAME/\$(git rev-parse --short HEAD)
aws s3 cp --content-type text/html --quiet final_report.html \$S3_PATH/final_report.html || :
"""
}
}

void pushArtifactFile(String FILE_NAME) {
echo "Push $FILE_NAME file to S3!"

Expand Down Expand Up @@ -148,24 +158,6 @@ void markPassedTests() {
}
}

void printKubernetesStatus(String LOCATION, String CLUSTER_SUFFIX) {
sh """
export KUBECONFIG=/tmp/${CLUSTER_NAME}-${CLUSTER_SUFFIX}
echo "========== KUBERNETES STATUS $LOCATION TEST =========="
gcloud container clusters list|grep -E "NAME|${CLUSTER_NAME}-${CLUSTER_SUFFIX} "
echo
kubectl get nodes
echo
kubectl top nodes
echo
kubectl get pods --all-namespaces
echo
kubectl top pod --all-namespaces
echo
kubectl get events --field-selector type!=Normal --all-namespaces --sort-by=".lastTimestamp"
echo "======================================================"
"""
}

String formatTime(def time) {
if (!time || time == "N/A") return "N/A"
Expand All @@ -185,7 +177,6 @@ String formatTime(def time) {
}

@Field def TestsReport = '| Test Name | Result | Time |\r\n| ----------- | -------- | ------ |'
@Field def TestsReportXML = '<testsuite name=\\"PSMDB\\">\n'

void makeReport() {
def wholeTestAmount = tests.size()
Expand All @@ -206,17 +197,62 @@ void makeReport() {
startedTestAmount++
}
TestsReport = TestsReport + "\r\n| " + testName + " | [" + testResult + "](" + testUrl + ") | " + formatTime(testTime) + " |"
TestsReportXML = TestsReportXML + '<testcase name=\\"' + testName + '\\" time=\\"' + testTime + '\\"><'+ testResult +'/></testcase>\n'
}
TestsReport = TestsReport + "\r\n\r\n| Summary | Value |\r\n| ------- | ----- |"
TestsReport = TestsReport + "\r\n| Tests Run | $startedTestAmount/$wholeTestAmount |"
TestsReport = TestsReport + "\r\n| Job Duration | " + formatTime(currentBuild.duration / 1000) + " |"
TestsReport = TestsReport + "\r\n| Total Test Time | " + formatTime(totalTestTime) + " |"
TestsReportXML = TestsReportXML + '</testsuite>\n'
}

sh """
echo "${TestsReportXML}" > TestsReport.xml
"""
void generateMissingReports() {
sh "mkdir -p e2e-tests/reports"

for (int i = 0; i < tests.size(); i++) {
def testName = tests[i]["name"]
def testResult = tests[i]["result"]
def testTime = tests[i]["time"] ?: 0

if (testResult == "skipped") {
continue
}

def xmlFile = "e2e-tests/reports/${testName}.xml"
def htmlFile = "e2e-tests/reports/${testName}.html"

if (!fileExists(xmlFile)) {
def failures = testResult == "failure" ? 1 : 0
def failureElement = testResult == "failure" ?
'<failure message="Missing report">Test did not produce a report</failure>' : ''

writeFile file: xmlFile, text: """<?xml version="1.0" encoding="utf-8"?>
<testsuites name="pytest tests">
<testsuite name="psmdb-e2e" errors="0" failures="${failures}" skipped="0" tests="1" time="${testTime}">
<testcase classname="e2e-tests.${testName}" name="${testName}" time="${testTime}">
${failureElement}
</testcase>
</testsuite>
</testsuites>"""
}

if (!fileExists(htmlFile)) {
def resultCapitalized = testResult == "failure" ? "Failed" : "Passed"
def formattedTime = formatTime(testTime)
def logMessage = testResult == "failure" ?
"Test did not produce a report" :
"Test marked as passed (from previous run)"

writeFile file: htmlFile, text: """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title id="head-title">${testName}.html</title>
</head>
<body>
<div id="data-container" data-jsonblob='{"environment": {"Note": "Placeholder report generated because the test report was missing"}, "tests": {"${testName}": [{"extras": [], "result": "${resultCapitalized}", "testId": "${testName}", "duration": "${formattedTime}", "resultsTableRow": ["<td class=\\"col-result\\">${resultCapitalized}</td>", "<td>-</td>", "<td class=\\"col-testId\\">${testName}</td>", "<td class=\\"col-duration\\">${formattedTime}</td>", "<td class=\\"col-links\\"></td>"], "log": "${logMessage}"}]}}'></div>
</body>
</html>"""
}
}
}

void clusterRunner(String cluster) {
Expand All @@ -242,48 +278,48 @@ void clusterRunner(String cluster) {
}

void runTest(Integer TEST_ID) {
def retryCount = 0
def testName = tests[TEST_ID]["name"]
def clusterSuffix = tests[TEST_ID]["cluster"]
def timeStart = new Date().getTime()

waitUntil {
def timeStart = new Date().getTime()
try {
echo "The $testName test was started on cluster ${CLUSTER_NAME}-${clusterSuffix} !"
tests[TEST_ID]["result"] = "failure"

timeout(time: 90, unit: 'MINUTES') {
sh """
if [ $retryCount -eq 0 ]; then
export DEBUG_TESTS=0
else
export DEBUG_TESTS=1
fi
export KUBECONFIG=/tmp/${CLUSTER_NAME}-${clusterSuffix}
time ./e2e-tests/$testName/run
"""
}
pushArtifactFile("${env.GIT_BRANCH}-${env.GIT_SHORT_COMMIT}-$testName")
tests[TEST_ID]["result"] = "passed"
return true
}
catch (exc) {
printKubernetesStatus("AFTER","$clusterSuffix")
echo "Test $testName has failed!"
if (retryCount >= 1 || currentBuild.nextBuild != null) {
currentBuild.result = 'FAILURE'
return true
}
retryCount++
return false
}
finally {
def timeStop = new Date().getTime()
def durationSec = (timeStop - timeStart) / 1000
tests[TEST_ID]["time"] = durationSec
pushLogFile("$testName")
echo "The $testName test was finished!"
try {
echo "The $testName test was started on cluster ${CLUSTER_NAME}-${clusterSuffix} !"
tests[TEST_ID]["result"] = "failure"

timeout(time: 90, unit: 'MINUTES') {
sh """
export DEBUG_TESTS=1
export KUBECONFIG=/tmp/${CLUSTER_NAME}-${clusterSuffix}
export PATH="\$HOME/.local/bin:\$PATH"
mkdir -p e2e-tests/reports

Comment on lines +291 to +295
REPORT_OPTS="--html=e2e-tests/reports/${testName}.html --junitxml=e2e-tests/reports/${testName}.xml"

# Run native pytest if test_*.py exists, otherwise run bash via wrapper
if ls e2e-tests/$testName/test_*.py 1>/dev/null 2>&1; then
uv run pytest e2e-tests/$testName/ \$REPORT_OPTS
else
uv run pytest e2e-tests/test_pytest_wrapper.py --test-name=$testName \$REPORT_OPTS
fi
"""
}
pushArtifactFile("${env.GIT_BRANCH}-${env.GIT_SHORT_COMMIT}-$testName")
tests[TEST_ID]["result"] = "passed"
}
catch (org.jenkinsci.plugins.workflow.steps.FlowInterruptedException exc) {
echo "Test $testName was interrupted!"
throw exc
}
catch (exc) {
echo "Test $testName has failed!"
currentBuild.result = 'FAILURE'
}
finally {
def timeStop = new Date().getTime()
def durationSec = (timeStop - timeStart) / 1000
tests[TEST_ID]["time"] = durationSec
pushLogFile("$testName")
echo "The $testName test was finished!"
}
}

Expand All @@ -309,6 +345,10 @@ EOF
sudo yum install -y google-cloud-cli google-cloud-cli-gke-gcloud-auth-plugin

curl -sL https://github.com/mitchellh/golicense/releases/latest/download/golicense_0.2.0_linux_x86_64.tar.gz | sudo tar -C /usr/local/bin -xzf - golicense

curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="\$HOME/.local/bin:\$PATH"
uv sync --locked
"""
installAzureCLI()
azureAuth()
Expand Down Expand Up @@ -431,7 +471,7 @@ pipeline {
GIT_SHORT_COMMIT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
VERSION = "${env.GIT_BRANCH}-${env.GIT_SHORT_COMMIT}"
CLUSTER_NAME = sh(script: "echo jen-psmdb-${env.CHANGE_ID}-${GIT_SHORT_COMMIT}-${env.BUILD_NUMBER} | tr '[:upper:]' '[:lower:]'", returnStdout: true).trim()
AUTHOR_NAME = sh(script: "echo ${CHANGE_AUTHOR_EMAIL} | awk -F'@' '{print \$1}'", , returnStdout: true).trim()
AUTHOR_NAME = sh(script: "echo ${CHANGE_AUTHOR_EMAIL} | awk -F'@' '{print \$1}'", returnStdout: true).trim()
ENABLE_LOGGING = "true"
}
agent {
Expand Down Expand Up @@ -463,7 +503,7 @@ pipeline {
prepareNode()
script {
if (AUTHOR_NAME == 'null') {
AUTHOR_NAME = sh(script: "git show -s --pretty=%ae | awk -F'@' '{print \$1}'", , returnStdout: true).trim()
AUTHOR_NAME = sh(script: "git show -s --pretty=%ae | awk -F'@' '{print \$1}'", returnStdout: true).trim()
}
for (comment in pullRequest.comments) {
println("Author: ${comment.user}, Comment: ${comment.body}")
Expand Down Expand Up @@ -680,12 +720,20 @@ pipeline {
}
}
makeReport()
junit testResults: '*.xml', healthScaleFactor: 1.0
archiveArtifacts '*.xml'
generateMissingReports()

sh """
export PATH="\$HOME/.local/bin:\$PATH"
uv run pytest_html_merger -i e2e-tests/reports -o final_report.html
uv run junitparser merge --glob 'e2e-tests/reports/*.xml' final_report.xml
"""
junit testResults: 'final_report.xml', healthScaleFactor: 1.0
archiveArtifacts 'final_report.xml, final_report.html'
pushReportFile()

unstash 'IMAGE'
def IMAGE = sh(returnStdout: true, script: "cat results/docker/TAG").trim()
TestsReport = TestsReport + "\r\n\r\ncommit: ${env.CHANGE_URL}/commits/${env.GIT_COMMIT}\r\nimage: `${IMAGE}`\r\n"
TestsReport = TestsReport + "\r\n\r\nCommit: ${env.CHANGE_URL}/commits/${env.GIT_COMMIT}\r\nImage: `${IMAGE}`\r\nTest report: [report](${testUrlPrefix}/${env.GIT_BRANCH}/${env.GIT_SHORT_COMMIT}/final_report.html)\r\n"
pullRequest.comment(TestsReport)
}
deleteOldClusters("$CLUSTER_NAME")
Expand Down
20 changes: 20 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ undeploy: ## Undeploy operator
test: envtest generate ## Run tests.
DISABLE_TELEMETRY=true KUBEBUILDER_ASSETS="$(shell $(ENVTEST) --arch=amd64 use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out

py-deps: uv ## Install e2e-tests Python dependencies
$(UV) sync --locked

py-update-deps: uv ## Update e2e-tests Python dependencies
$(UV) lock --upgrade

py-fmt: uv ## Format and organize imports in e2e-tests
$(UV) run ruff check --select I --fix e2e-tests/
$(UV) run ruff format e2e-tests/

py-check: uv ## Run ruff and mypy checks on e2e-tests
$(UV) run ruff check e2e-tests/
$(UV) run mypy e2e-tests/

# go-get-tool will 'go get' any package $2 and install it to $1.
PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
define go-get-tool
Expand Down Expand Up @@ -105,6 +119,12 @@ MOCKGEN = $(shell pwd)/bin/mockgen
mockgen: ## Download mockgen locally if necessary.
$(call go-get-tool,$(MOCKGEN), github.com/golang/mock/mockgen@latest)

UV = $(shell pwd)/bin/uv
uv: ## Download uv locally if necessary.
@[ -f $(UV) ] || { \
set -e ;\
curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=$(PROJECT_DIR)/bin sh ;\
}
update-version:
echo $(NEXT_VER) > pkg/version/version.txt

Expand Down
Loading
Loading