Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
185 changes: 185 additions & 0 deletions eng/pipelines/mssql-odbc-daily-validation-pipeline.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
name: mssql-odbc-daily-$(Date:yyyyMMdd)$(Rev:.r)

trigger: none
pr: none

schedules:
- cron: '0 4 * * *'
displayName: 'Daily mssql-odbc validation'
branches:
include:
- main
always: true

jobs:
- job: PytestOnMssqlOdbc
displayName: 'Complete mssql-python suite on mssql-odbc'
# Headroom over the serial step budgets (SQL setup 22m + pytest 130m + cleanup 3m)
# plus unbudgeted deps-install/build/install-core/verify/publish, so the always()
# publish and cleanup steps still run in the worst case.
timeoutInMinutes: 240
pool:
vmImage: 'ubuntu-latest'

variables:
testContainer: 'mssql-python-rust-odbc-$(Build.BuildId)'
sqlContainer: 'sqlserver-rust-odbc-$(Build.BuildId)'

steps:
- checkout: self
clean: true
fetchDepth: 1
displayName: 'Checkout latest GitHub main'

- script: |
docker run -d --name $(testContainer) \
-v $(Build.SourcesDirectory):/workspace \
-w /workspace \
--network bridge \
ubuntu:24.04 \
tail -f /dev/null
displayName: 'Create mssql-odbc test container'

- script: |
python3 eng/scripts/setup_sql_container.py \
--name "$(sqlContainer)" --owner "$(Build.BuildId).$(System.JobId)" \
--image "mcr.microsoft.com/mssql/server:2022-latest" --database TestDB
displayName: 'Start SQL Server for mssql-odbc tests'
timeoutInMinutes: 22
env:
DB_PASSWORD: $(DB_PASSWORD)

- script: |
docker exec $(testContainer) bash -c '
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive TZ=UTC
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime
echo $TZ > /etc/timezone
apt-get update
apt-get install -y --no-install-recommends \
python3 python3-pip python3-venv python3-full python3-dev \
cmake build-essential pybind11-dev unixodbc-dev curl ca-certificates \
libssl3 libgssapi-krb5-2 libkrb5-3
Comment thread
gargsaumya marked this conversation as resolved.
python3 -m venv /opt/venv
source /opt/venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
'
displayName: 'Install mssql-odbc test dependencies'

- script: |
docker exec $(testContainer) bash -c '
set -euo pipefail
source /opt/venv/bin/activate
cd mssql_python/pybind
chmod +x build.sh
./build.sh
'
displayName: 'Build mssql-python native bindings'

- template: steps/install-mssql-py-core.yml
parameters:
platform: container
containerName: $(testContainer)
venvActivate: 'source /opt/venv/bin/activate'
displaySuffix: ' for mssql-odbc tests'

- script: |
set -uo pipefail
SQLSERVER_IP=$(docker inspect $(sqlContainer) --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')
export DB_CONNECTION_STRING="Server=$SQLSERVER_IP;Database=TestDB;Uid=SA;Pwd=$DB_PASSWORD;TrustServerCertificate=yes"
Comment thread
gargsaumya marked this conversation as resolved.
rc=0
output=$(docker exec \
-e MSSQL_PYTHON_NATIVE_PROVIDER=mssql-odbc \
-e DB_CONNECTION_STRING \
-e PYTHONPATH=/workspace \
$(testContainer) bash -c '
set -euo pipefail
source /opt/venv/bin/activate
python eng/scripts/verify_mssql_odbc_provider.py
' 2>&1) || rc=$?
printf '%s\n' "$output"
if ! grep -q 'MSSQL_ODBC_PREFLIGHT_OK' <<< "$output"; then
echo "##[error]mssql-odbc provider preflight failed (exit $rc)."
exit 1
fi
displayName: 'Verify packaged mssql-odbc selection, load, and query'
env:
DB_PASSWORD: $(DB_PASSWORD)

- script: |
set -uo pipefail
SQLSERVER_IP=$(docker inspect $(sqlContainer) --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')
if [ -z "$SQLSERVER_IP" ]; then
echo "##[error]Could not resolve SQL Server container IP; SQL Server is unreachable (infrastructure failure)."
exit 1
fi
export DB_CONNECTION_STRING="Server=$SQLSERVER_IP;Database=TestDB;Uid=SA;Pwd=$DB_PASSWORD;TrustServerCertificate=yes"
status_file="$(Build.SourcesDirectory)/test-results/mssql-odbc/runner.status"
rm -f "$status_file"
rc=0
docker exec \
-e MSSQL_PYTHON_NATIVE_PROVIDER=mssql-odbc \
-e DB_CONNECTION_STRING \
-e TEST_RESULTS_DIR=/workspace/test-results/mssql-odbc \
-e TEST_STATUS_FILE=/workspace/test-results/mssql-odbc/runner.status \
-e PYTEST_FILE_TIMEOUT=10m \
-e PYTEST_TOTAL_BUDGET=110m \
-e PYTHONPATH=/workspace \
$(testContainer) bash -c '
source /opt/venv/bin/activate
chmod +x eng/scripts/run-mssql-odbc-tests.sh
eng/scripts/run-mssql-odbc-tests.sh
' || rc=$?

status=$(cat "$status_file" 2>/dev/null || true)
case "$rc" in
0)
if [ "$status" != success ]; then
echo "##vso[task.logissue type=error]The pytest process returned success without a valid runner status."
exit 1
fi
echo 'The complete pytest suite passed with mssql-odbc.'
;;
1)
if [ "$status" != advisory ]; then
echo '##vso[task.logissue type=error]The test container failed before the pytest runner reported an advisory result.'
exit 1
fi
echo '##vso[task.logissue type=warning]The complete pytest suite has expected compatibility failures with mssql-odbc; see published test results.'
echo '##vso[task.complete result=SucceededWithIssues;]'
;;
*)
echo "##vso[task.logissue type=error]The mssql-odbc pytest harness failed (exit $rc)."
exit 1
;;
esac
exit 0
displayName: 'Run complete pytest suite with mssql-odbc (advisory)'
timeoutInMinutes: 130
env:
DB_PASSWORD: $(DB_PASSWORD)

- task: PublishTestResults@2
displayName: 'Publish mssql-odbc compatibility results'
condition: always()
inputs:
testResultsFormat: JUnit
testResultsFiles: 'test-results/mssql-odbc/*.xml'
searchFolder: '$(Build.SourcesDirectory)'
testRunTitle: 'mssql-python complete suite on mssql-odbc'
mergeTestResults: true
failTaskOnFailedTests: false
publishRunAttachments: true

- script: |
cleanup_rc=0
python3 eng/scripts/setup_sql_container.py --cleanup \
--name "$(sqlContainer)" --owner "$(Build.BuildId).$(System.JobId)" || cleanup_rc=$?
docker rm -f $(testContainer) || true
exit "$cleanup_rc"
displayName: 'Clean up mssql-odbc test containers'
condition: always()
timeoutInMinutes: 3
env:
DB_PASSWORD: $(DB_PASSWORD)
199 changes: 199 additions & 0 deletions eng/scripts/run-mssql-odbc-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#!/bin/bash
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

# Run every mssql-python test file in an isolated process with mssql-odbc
# selected. Functional failures are advisory, but crashes and timeouts still
# produce JUnit entries so the pipeline reports the complete result.

set -uo pipefail

RESULTS_DIR="${TEST_RESULTS_DIR:-test-results/mssql-odbc}"
FILE_TIMEOUT="${PYTEST_FILE_TIMEOUT:-10m}"
TOTAL_BUDGET="${PYTEST_TOTAL_BUDGET:-110m}"
STATUS_FILE="${TEST_STATUS_FILE:-$RESULTS_DIR/runner.status}"
KILL_GRACE_SECONDS=10

if ! mkdir -p "$RESULTS_DIR"; then
echo "##[error]Could not create test results directory: $RESULTS_DIR"
exit 2
fi
rm -f "$RESULTS_DIR"/*.xml
rm -f "$STATUS_FILE"

finish() {
local code="$1" status="$2"
printf '%s\n' "$status" > "$STATUS_FILE" || exit 2
exit "$code"
}

on_exit() {
local code=$?
if [ ! -s "$STATUS_FILE" ]; then
printf 'harness\n' > "$STATUS_FILE" 2>/dev/null || true
fi
return "$code"
}
trap on_exit EXIT

if [ "${MSSQL_PYTHON_NATIVE_PROVIDER:-}" != "mssql-odbc" ]; then
echo "##[error]MSSQL_PYTHON_NATIVE_PROVIDER must be set to mssql-odbc"
finish 2 harness
fi

xml_escape() {
printf '%s' "$1" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g' -e 's/"/\&quot;/g'
}

write_stub() {
local name="$1" kind="$2" reason="$3" report="$4"
local safe_name safe_reason
safe_name="$(xml_escape "$name")"
safe_reason="$(xml_escape "$reason")"
if [ "$kind" = "skipped" ]; then
cat > "$report" <<XML
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
<testsuite name="$safe_name" tests="1" errors="0" failures="0" skipped="1" time="0">
<testcase classname="mssql_odbc.$safe_name" name="pytest_process" time="0">
<skipped type="pytest.skip" message="$safe_reason"/>
</testcase>
</testsuite>
</testsuites>
XML
else
cat > "$report" <<XML
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
<testsuite name="$safe_name" tests="1" errors="1" failures="0" skipped="0" time="0">
<testcase classname="mssql_odbc.$safe_name" name="pytest_process" time="0">
<error type="ProcessTerminated" message="$safe_reason">The pytest process terminated before producing a report.</error>
</testcase>
</testsuite>
</testsuites>
XML
fi
}

to_seconds() {
local value="$1" number="${1%[smh]}"
case "$value" in
*h) echo $((number * 3600)) ;;
*m) echo $((number * 60)) ;;
*s) echo "$number" ;;
*) echo "$value" ;;
esac
}

report_is_valid() {
python - "$1" <<'PY'
import sys
from xml.etree import ElementTree

ElementTree.parse(sys.argv[1])
PY
}

mapfile -t TEST_FILES < <(find tests -name 'test_*.py' -type f | sort)
if [ "${#TEST_FILES[@]}" -eq 0 ] || ! python -m pytest --version >/dev/null 2>&1; then
echo "##[error]The pytest harness is not usable"
finish 2 harness
fi

FILE_BUDGET_SECONDS="$(to_seconds "$FILE_TIMEOUT")"
TOTAL_BUDGET_SECONDS="$(to_seconds "$TOTAL_BUDGET")"
passed=0
failed=0
crashed=0
timed_out=0
skipped=0

echo "Running ${#TEST_FILES[@]} test files with MSSQL_PYTHON_NATIVE_PROVIDER=${MSSQL_PYTHON_NATIVE_PROVIDER:-unset}"

Comment thread
gargsaumya marked this conversation as resolved.
for index in "${!TEST_FILES[@]}"; do
test_file="${TEST_FILES[$index]}"
name="${test_file#tests/}"
name="${name%.py}"
name="${name//\//_}"
report="$RESULTS_DIR/results-$name.xml"
remaining=$((TOTAL_BUDGET_SECONDS - SECONDS))

if [ "$remaining" -le "$KILL_GRACE_SECONDS" ]; then
remaining_files=$((${#TEST_FILES[@]} - index))
for rest_index in $(seq "$index" $((${#TEST_FILES[@]} - 1))); do
rest_file="${TEST_FILES[$rest_index]}"
rest_name="${rest_file#tests/}"
rest_name="${rest_name%.py}"
rest_name="${rest_name//\//_}"
write_stub "$rest_name" error "Total test budget of $TOTAL_BUDGET exhausted before this file ran" \
"$RESULTS_DIR/results-$rest_name.xml"
done
timed_out=$((timed_out + remaining_files))
break
fi

slice="$FILE_BUDGET_SECONDS"
if [ "$slice" -gt "$((remaining - KILL_GRACE_SECONDS))" ]; then
slice=$((remaining - KILL_GRACE_SECONDS))
fi

echo "##[group]$test_file"
timeout -k "${KILL_GRACE_SECONDS}s" "${slice}s" \
python -m pytest "$test_file" -v --junitxml="$report" \
--capture=tee-sys --cache-clear
rc=$?
echo "##[endgroup]"

case "$rc" in
0)
passed=$((passed + 1))
;;
1)
failed=$((failed + 1))
;;
2|3|4)
echo "##[error]The pytest harness failed on $test_file (exit $rc)"
finish 2 harness
;;
5)
write_stub "$name" skipped "No tests collected" "$report"
skipped=$((skipped + 1))
;;
124|137)
timed_out=$((timed_out + 1))
;;
125|126|127)
echo "##[error]The pytest harness could not execute $test_file (exit $rc)"
finish 2 harness
;;
*)
crashed=$((crashed + 1))
;;
esac

if [ ! -s "$report" ] || ! report_is_valid "$report" >/dev/null 2>&1; then
if [ "$rc" -eq 0 ]; then
echo "##[error]Pytest reported success for $test_file but produced no valid JUnit results (harness failure)"
finish 2 harness
fi
write_stub "$name" error "Pytest exited $rc without producing valid JUnit" "$report"
fi
if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ] || [ "$rc" -gt 127 ]; then
write_stub "${name}_process" error "Pytest process exited $rc" \
"$RESULTS_DIR/results-$name-process.xml"
fi
Comment thread
Copilot marked this conversation as resolved.
done

echo "files: ${#TEST_FILES[@]} | passed: $passed | failed: $failed | crashed: $crashed | timed out: $timed_out | skipped: $skipped"

if [ "$((passed + failed + crashed + timed_out))" -eq 0 ]; then
echo "##[error]No test file executed any tests"
finish 2 harness
fi

# Intentional no-test files (pytest exit 5, e.g. stress files excluded by the
# "not stress" marker) count as skipped and must not mark the run SucceededWithIssues.
if [ "$failed" -gt 0 ] || [ "$crashed" -gt 0 ] || [ "$timed_out" -gt 0 ]; then
finish 1 advisory
fi
finish 0 success
Loading
Loading