From a668a00d0bdf820bd5c37e2ad6bf847f28dd75e3 Mon Sep 17 00:00:00 2001 From: Nilesh Choudhary Date: Tue, 12 May 2026 16:28:56 +0100 Subject: [PATCH 1/8] Create pipeline-unit-tests.yml added unit test --- .Pipelines/pipeline-unit-tests.yml | 59 ++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 .Pipelines/pipeline-unit-tests.yml diff --git a/.Pipelines/pipeline-unit-tests.yml b/.Pipelines/pipeline-unit-tests.yml new file mode 100644 index 00000000..fc2ca428 --- /dev/null +++ b/.Pipelines/pipeline-unit-tests.yml @@ -0,0 +1,59 @@ +# Manual ADO pipeline that runs the msal Python unit tests only. +# No lab cert, no e2e — those live in azure-pipelines.yml / future e2e pipeline. + +trigger: none +pr: none + +stages: +- stage: UnitTests + displayName: 'Run unit tests' + jobs: + - job: Test + displayName: 'pytest (unit only)' + pool: + vmImage: ubuntu-latest + strategy: + matrix: + Python39: + python.version: '3.9' + Python310: + python.version: '3.10' + Python311: + python.version: '3.11' + Python312: + python.version: '3.12' + Python313: + python.version: '3.13' + Python314: + python.version: '3.14' + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: '$(python.version)' + displayName: 'Set up Python' + + - script: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-azurepipelines + displayName: 'Install dependencies' + + - bash: | + mkdir -p test-results + set -o pipefail + pytest -vv \ + --junitxml=test-results/junit.xml \ + --ignore=tests/test_e2e.py \ + --ignore=tests/test_e2e_manual.py \ + --ignore=tests/test_fmi_e2e.py \ + 2>&1 | tee test-results/pytest.log + displayName: 'Run unit tests' + + - task: PublishTestResults@2 + displayName: 'Publish test results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'test-results/junit.xml' + failTaskOnFailedTests: true + testRunTitle: 'Python $(python.version)' From 3939c9cbd39a47b6962ffc20ef34277f3d2501a7 Mon Sep 17 00:00:00 2001 From: Nilesh Choudhary Date: Tue, 12 May 2026 16:40:02 +0100 Subject: [PATCH 2/8] Update pipeline-unit-tests.yml --- .Pipelines/pipeline-unit-tests.yml | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/.Pipelines/pipeline-unit-tests.yml b/.Pipelines/pipeline-unit-tests.yml index fc2ca428..cc92d53d 100644 --- a/.Pipelines/pipeline-unit-tests.yml +++ b/.Pipelines/pipeline-unit-tests.yml @@ -1,7 +1,10 @@ -# Manual ADO pipeline that runs the msal Python unit tests only. +# ADO pipeline that runs the msal Python unit tests only. # No lab cert, no e2e — those live in azure-pipelines.yml / future e2e pipeline. -trigger: none +trigger: + branches: + include: + - 4gust/ado-pipeline pr: none stages: @@ -11,7 +14,7 @@ stages: - job: Test displayName: 'pytest (unit only)' pool: - vmImage: ubuntu-latest + vmImage: ubuntu-22.04 strategy: matrix: Python39: @@ -27,10 +30,19 @@ stages: Python314: python.version: '3.14' steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '$(python.version)' - displayName: 'Set up Python' + # Install Python via uv (single-binary installer, no GitHub token needed). + # Replaces UsePythonVersion@0 to avoid the anonymous-download warning, + # and supports 3.14 today even when the hosted toolcache lags. + - bash: | + set -euo pipefail + curl -LsSf https://astral.sh/uv/install.sh | sh + export PATH="$HOME/.local/bin:$PATH" + uv python install $(python.version) + PY="$(uv python find $(python.version))" + echo "Resolved interpreter: $PY" + echo "##vso[task.prependpath]$(dirname $PY)" + echo "##vso[task.prependpath]$HOME/.local/bin" + displayName: 'Install Python $(python.version) via uv' - script: | python -m pip install --upgrade pip From c0025321cb3ed4b9b61642b7aed5a8f1226bc90d Mon Sep 17 00:00:00 2001 From: Nilesh Choudhary Date: Tue, 12 May 2026 16:42:35 +0100 Subject: [PATCH 3/8] Update pipeline-unit-tests.yml --- .Pipelines/pipeline-unit-tests.yml | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/.Pipelines/pipeline-unit-tests.yml b/.Pipelines/pipeline-unit-tests.yml index cc92d53d..24003f42 100644 --- a/.Pipelines/pipeline-unit-tests.yml +++ b/.Pipelines/pipeline-unit-tests.yml @@ -27,22 +27,18 @@ stages: python.version: '3.12' Python313: python.version: '3.13' - Python314: - python.version: '3.14' + # Python314 disabled: not yet in the ubuntu-22.04 hosted toolcache. + # UsePythonVersion@0 would have to download it from actions/python-versions + # (anonymous → rate-limited, needs a GitHub token), and uv-installed + # 3.14 is PEP 668 externally-managed so `pip install` fails. Re-enable + # this row once the hosted agent ships 3.14 preinstalled. + # Python314: + # python.version: '3.14' steps: - # Install Python via uv (single-binary installer, no GitHub token needed). - # Replaces UsePythonVersion@0 to avoid the anonymous-download warning, - # and supports 3.14 today even when the hosted toolcache lags. - - bash: | - set -euo pipefail - curl -LsSf https://astral.sh/uv/install.sh | sh - export PATH="$HOME/.local/bin:$PATH" - uv python install $(python.version) - PY="$(uv python find $(python.version))" - echo "Resolved interpreter: $PY" - echo "##vso[task.prependpath]$(dirname $PY)" - echo "##vso[task.prependpath]$HOME/.local/bin" - displayName: 'Install Python $(python.version) via uv' + - task: UsePythonVersion@0 + inputs: + versionSpec: '$(python.version)' + displayName: 'Set up Python' - script: | python -m pip install --upgrade pip From 0bc295870012b5440f95a76ed400a091b491ed4c Mon Sep 17 00:00:00 2001 From: Nilesh Choudhary Date: Tue, 12 May 2026 16:49:12 +0100 Subject: [PATCH 4/8] Update pipeline-unit-tests.yml --- .Pipelines/pipeline-unit-tests.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.Pipelines/pipeline-unit-tests.yml b/.Pipelines/pipeline-unit-tests.yml index 24003f42..eed8bd9e 100644 --- a/.Pipelines/pipeline-unit-tests.yml +++ b/.Pipelines/pipeline-unit-tests.yml @@ -27,13 +27,8 @@ stages: python.version: '3.12' Python313: python.version: '3.13' - # Python314 disabled: not yet in the ubuntu-22.04 hosted toolcache. - # UsePythonVersion@0 would have to download it from actions/python-versions - # (anonymous → rate-limited, needs a GitHub token), and uv-installed - # 3.14 is PEP 668 externally-managed so `pip install` fails. Re-enable - # this row once the hosted agent ships 3.14 preinstalled. - # Python314: - # python.version: '3.14' + Python314: + python.version: '3.14' steps: - task: UsePythonVersion@0 inputs: From 20490207d82831917c9c461b2413c258a778663e Mon Sep 17 00:00:00 2001 From: Nilesh Choudhary Date: Tue, 12 May 2026 16:56:18 +0100 Subject: [PATCH 5/8] Update pipeline-unit-tests.yml --- .Pipelines/pipeline-unit-tests.yml | 112 ++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 10 deletions(-) diff --git a/.Pipelines/pipeline-unit-tests.yml b/.Pipelines/pipeline-unit-tests.yml index eed8bd9e..50edec66 100644 --- a/.Pipelines/pipeline-unit-tests.yml +++ b/.Pipelines/pipeline-unit-tests.yml @@ -1,5 +1,7 @@ -# ADO pipeline that runs the msal Python unit tests only. -# No lab cert, no e2e — those live in azure-pipelines.yml / future e2e pipeline. +# ADO pipeline that runs the msal Python test suite. +# Unit tests run first (no secrets); E2E tests run after, with the MSID Lab +# certificate fetched from Key Vault. Triggered on every push to the +# working branch. trigger: branches: @@ -8,11 +10,15 @@ trigger: pr: none stages: + +# ───────────────────────────────────────────────────────────────────────────── +# Stage 1 · Unit tests — no Key Vault, no service connection. +# ───────────────────────────────────────────────────────────────────────────── - stage: UnitTests - displayName: 'Run unit tests' + displayName: 'Unit tests' jobs: - - job: Test - displayName: 'pytest (unit only)' + - job: Pytest + displayName: 'pytest (unit)' pool: vmImage: ubuntu-22.04 strategy: @@ -31,15 +37,15 @@ stages: python.version: '3.14' steps: - task: UsePythonVersion@0 + displayName: 'Use Python $(python.version)' inputs: versionSpec: '$(python.version)' - displayName: 'Set up Python' - script: | python -m pip install --upgrade pip pip install -r requirements.txt pip install pytest pytest-azurepipelines - displayName: 'Install dependencies' + displayName: 'Install Python dependencies' - bash: | mkdir -p test-results @@ -50,13 +56,99 @@ stages: --ignore=tests/test_e2e_manual.py \ --ignore=tests/test_fmi_e2e.py \ 2>&1 | tee test-results/pytest.log - displayName: 'Run unit tests' + displayName: 'Run pytest (unit only)' - task: PublishTestResults@2 - displayName: 'Publish test results' + displayName: 'Publish JUnit test results' condition: succeededOrFailed() inputs: testResultsFormat: 'JUnit' testResultsFiles: 'test-results/junit.xml' failTaskOnFailedTests: true - testRunTitle: 'Python $(python.version)' + testRunTitle: 'Unit tests · Python $(python.version)' + +# ───────────────────────────────────────────────────────────────────────────── +# Stage 2 · E2E tests — runs only if unit tests pass. Fetches the MSID Lab +# certificate from Key Vault (mirrors MSAL.NET's +# build/template-install-keyvault-secrets.yaml). +# ───────────────────────────────────────────────────────────────────────────── +- stage: E2ETests + displayName: 'E2E tests' + dependsOn: UnitTests + condition: succeeded() + jobs: + - job: Pytest + displayName: 'pytest (E2E)' + pool: + vmImage: ubuntu-22.04 + strategy: + matrix: + Python39: + python.version: '3.9' + Python310: + python.version: '3.10' + Python311: + python.version: '3.11' + Python312: + python.version: '3.12' + Python313: + python.version: '3.13' + Python314: + python.version: '3.14' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python $(python.version)' + inputs: + versionSpec: '$(python.version)' + + - script: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-azurepipelines + displayName: 'Install Python dependencies' + + - task: AzureKeyVault@2 + displayName: 'Fetch MSID Lab certificate from Key Vault' + inputs: + azureSubscription: 'AuthSdkResourceManager' + KeyVaultName: 'msidlabs' + SecretsFilter: 'LabAuth' + RunAsPreJob: false + + - bash: | + set -euo pipefail + if [ -z "${LAB_AUTH_B64:-}" ]; then + echo "##vso[task.logissue type=error]LabAuth secret is empty — Key Vault retrieval failed." + exit 1 + fi + CERT_PATH="$(Agent.TempDirectory)/lab-auth.pfx" + printf '%s' "$LAB_AUTH_B64" | base64 -d > "$CERT_PATH" + echo "##vso[task.setvariable variable=LAB_APP_CLIENT_CERT_PFX_PATH]$CERT_PATH" + echo "Lab cert written to: $CERT_PATH ($(wc -c < "$CERT_PATH") bytes)" + displayName: 'Decode lab certificate to PFX' + env: + LAB_AUTH_B64: $(LabAuth) + + - bash: | + mkdir -p test-results + set -o pipefail + pytest -vv \ + --junitxml=test-results/junit.xml \ + tests/test_e2e.py tests/test_fmi_e2e.py \ + 2>&1 | tee test-results/pytest.log + displayName: 'Run pytest (E2E only)' + env: + LAB_APP_CLIENT_CERT_PFX_PATH: $(LAB_APP_CLIENT_CERT_PFX_PATH) + + - task: PublishTestResults@2 + displayName: 'Publish JUnit test results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'test-results/junit.xml' + failTaskOnFailedTests: true + testRunTitle: 'E2E tests · Python $(python.version)' + + - bash: rm -f "$(Agent.TempDirectory)/lab-auth.pfx" + displayName: 'Remove lab certificate from agent' + condition: always() From 43e6b7339b1f4f384d20b09120df10e20669ca51 Mon Sep 17 00:00:00 2001 From: Nilesh Choudhary Date: Thu, 14 May 2026 10:45:12 +0100 Subject: [PATCH 6/8] Update pipeline-unit-tests.yml one session for e2e and one for unit for all versions --- .Pipelines/pipeline-unit-tests.yml | 210 +++++++++++++++++++---------- 1 file changed, 139 insertions(+), 71 deletions(-) diff --git a/.Pipelines/pipeline-unit-tests.yml b/.Pipelines/pipeline-unit-tests.yml index 50edec66..4cb1b25f 100644 --- a/.Pipelines/pipeline-unit-tests.yml +++ b/.Pipelines/pipeline-unit-tests.yml @@ -1,13 +1,19 @@ -# ADO pipeline that runs the msal Python test suite. -# Unit tests run first (no secrets); E2E tests run after, with the MSID Lab -# certificate fetched from Key Vault. Triggered on every push to the -# working branch. +# ADO pipeline for the msal Python test suite. +# Two stages → two GitHub checks (Unit tests, E2E tests). Each stage uses +# one job that loops the supported Python matrix internally so the GitHub +# check count stays small. trigger: branches: include: - 4gust/ado-pipeline -pr: none + - dev + +pr: + branches: + include: + - dev + drafts: false stages: @@ -18,54 +24,85 @@ stages: displayName: 'Unit tests' jobs: - job: Pytest - displayName: 'pytest (unit)' + displayName: 'pytest (unit, all Python versions)' pool: vmImage: ubuntu-22.04 - strategy: - matrix: - Python39: - python.version: '3.9' - Python310: - python.version: '3.10' - Python311: - python.version: '3.11' - Python312: - python.version: '3.12' - Python313: - python.version: '3.13' - Python314: - python.version: '3.14' + timeoutInMinutes: 30 steps: - task: UsePythonVersion@0 - displayName: 'Use Python $(python.version)' - inputs: - versionSpec: '$(python.version)' - - - script: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pytest pytest-azurepipelines - displayName: 'Install Python dependencies' + displayName: 'Use Python 3.9' + inputs: { versionSpec: '3.9' } + - task: UsePythonVersion@0 + displayName: 'Use Python 3.10' + inputs: { versionSpec: '3.10' } + - task: UsePythonVersion@0 + displayName: 'Use Python 3.11' + inputs: { versionSpec: '3.11' } + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: { versionSpec: '3.12' } + - task: UsePythonVersion@0 + displayName: 'Use Python 3.13' + inputs: { versionSpec: '3.13' } + - task: UsePythonVersion@0 + displayName: 'Use Python 3.14' + inputs: { versionSpec: '3.14' } - bash: | + set -uo pipefail mkdir -p test-results - set -o pipefail - pytest -vv \ - --junitxml=test-results/junit.xml \ - --ignore=tests/test_e2e.py \ - --ignore=tests/test_e2e_manual.py \ - --ignore=tests/test_fmi_e2e.py \ - 2>&1 | tee test-results/pytest.log - displayName: 'Run pytest (unit only)' + OVERALL_RC=0 + + for V in 3.9 3.10 3.11 3.12 3.13 3.14; do + echo "============================================================" + echo " Unit tests · Python ${V}" + echo "============================================================" + + PY="python${V}" + if ! command -v "$PY" >/dev/null; then + echo "##vso[task.logissue type=error]${PY} not on PATH" + OVERALL_RC=1 + continue + fi + + "$PY" -m venv ".venv-${V}" + # shellcheck disable=SC1090 + source ".venv-${V}/bin/activate" + + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-azurepipelines + + set -o pipefail + pytest -vv \ + --junitxml="test-results/junit-unit-${V}.xml" \ + --ignore=tests/test_e2e.py \ + --ignore=tests/test_e2e_manual.py \ + --ignore=tests/test_fmi_e2e.py \ + 2>&1 | tee "test-results/pytest-unit-${V}.log" + RC=$? + set +o pipefail + + deactivate + + if [ $RC -ne 0 ]; then + echo "Python ${V}: unit pytest exited ${RC}" + OVERALL_RC=1 + fi + done + + exit $OVERALL_RC + displayName: 'Run pytest (unit, all Python versions)' - task: PublishTestResults@2 displayName: 'Publish JUnit test results' condition: succeededOrFailed() inputs: testResultsFormat: 'JUnit' - testResultsFiles: 'test-results/junit.xml' + testResultsFiles: 'test-results/junit-unit-*.xml' failTaskOnFailedTests: true - testRunTitle: 'Unit tests · Python $(python.version)' + testRunTitle: 'Unit tests' + mergeTestResults: true # ───────────────────────────────────────────────────────────────────────────── # Stage 2 · E2E tests — runs only if unit tests pass. Fetches the MSID Lab @@ -78,34 +115,29 @@ stages: condition: succeeded() jobs: - job: Pytest - displayName: 'pytest (E2E)' + displayName: 'pytest (E2E, all Python versions)' pool: vmImage: ubuntu-22.04 - strategy: - matrix: - Python39: - python.version: '3.9' - Python310: - python.version: '3.10' - Python311: - python.version: '3.11' - Python312: - python.version: '3.12' - Python313: - python.version: '3.13' - Python314: - python.version: '3.14' + timeoutInMinutes: 60 steps: - task: UsePythonVersion@0 - displayName: 'Use Python $(python.version)' - inputs: - versionSpec: '$(python.version)' - - - script: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pytest pytest-azurepipelines - displayName: 'Install Python dependencies' + displayName: 'Use Python 3.9' + inputs: { versionSpec: '3.9' } + - task: UsePythonVersion@0 + displayName: 'Use Python 3.10' + inputs: { versionSpec: '3.10' } + - task: UsePythonVersion@0 + displayName: 'Use Python 3.11' + inputs: { versionSpec: '3.11' } + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: { versionSpec: '3.12' } + - task: UsePythonVersion@0 + displayName: 'Use Python 3.13' + inputs: { versionSpec: '3.13' } + - task: UsePythonVersion@0 + displayName: 'Use Python 3.14' + inputs: { versionSpec: '3.14' } - task: AzureKeyVault@2 displayName: 'Fetch MSID Lab certificate from Key Vault' @@ -130,13 +162,48 @@ stages: LAB_AUTH_B64: $(LabAuth) - bash: | + set -uo pipefail mkdir -p test-results - set -o pipefail - pytest -vv \ - --junitxml=test-results/junit.xml \ - tests/test_e2e.py tests/test_fmi_e2e.py \ - 2>&1 | tee test-results/pytest.log - displayName: 'Run pytest (E2E only)' + OVERALL_RC=0 + + for V in 3.9 3.10 3.11 3.12 3.13 3.14; do + echo "============================================================" + echo " E2E tests · Python ${V}" + echo "============================================================" + + PY="python${V}" + if ! command -v "$PY" >/dev/null; then + echo "##vso[task.logissue type=error]${PY} not on PATH" + OVERALL_RC=1 + continue + fi + + "$PY" -m venv ".venv-${V}" + # shellcheck disable=SC1090 + source ".venv-${V}/bin/activate" + + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-azurepipelines + + set -o pipefail + pytest -vv \ + --junitxml="test-results/junit-e2e-${V}.xml" \ + tests/test_e2e.py tests/test_fmi_e2e.py \ + 2>&1 | tee "test-results/pytest-e2e-${V}.log" + RC=$? + set +o pipefail + + deactivate + + if [ $RC -ne 0 ]; then + echo "Python ${V}: e2e pytest exited ${RC}" + OVERALL_RC=1 + fi + done + + exit $OVERALL_RC + displayName: 'Run pytest (E2E, all Python versions)' env: LAB_APP_CLIENT_CERT_PFX_PATH: $(LAB_APP_CLIENT_CERT_PFX_PATH) @@ -145,9 +212,10 @@ stages: condition: succeededOrFailed() inputs: testResultsFormat: 'JUnit' - testResultsFiles: 'test-results/junit.xml' + testResultsFiles: 'test-results/junit-e2e-*.xml' failTaskOnFailedTests: true - testRunTitle: 'E2E tests · Python $(python.version)' + testRunTitle: 'E2E tests' + mergeTestResults: true - bash: rm -f "$(Agent.TempDirectory)/lab-auth.pfx" displayName: 'Remove lab certificate from agent' From 97e7bfb2f8faec4256108797853583c79810e32c Mon Sep 17 00:00:00 2001 From: Nilesh Choudhary Date: Thu, 14 May 2026 10:57:04 +0100 Subject: [PATCH 7/8] Update pipeline-unit-tests.yml --- .Pipelines/pipeline-unit-tests.yml | 188 ++++++++++++++++++----------- 1 file changed, 116 insertions(+), 72 deletions(-) diff --git a/.Pipelines/pipeline-unit-tests.yml b/.Pipelines/pipeline-unit-tests.yml index 4cb1b25f..6b35926f 100644 --- a/.Pipelines/pipeline-unit-tests.yml +++ b/.Pipelines/pipeline-unit-tests.yml @@ -1,7 +1,7 @@ # ADO pipeline for the msal Python test suite. -# Two stages → two GitHub checks (Unit tests, E2E tests). Each stage uses -# one job that loops the supported Python matrix internally so the GitHub -# check count stays small. +# Two stages → two GitHub checks (Unit tests, E2E tests). Each stage is one +# job that fans out the Python matrix as parallel bash background processes +# so wall-clock time ≈ slowest single Python version, not the sum. trigger: branches: @@ -21,10 +21,10 @@ stages: # Stage 1 · Unit tests — no Key Vault, no service connection. # ───────────────────────────────────────────────────────────────────────────── - stage: UnitTests - displayName: 'Unit tests' + displayName: 'Unit test' jobs: - job: Pytest - displayName: 'pytest (unit, all Python versions)' + displayName: 'Unit test' pool: vmImage: ubuntu-22.04 timeoutInMinutes: 30 @@ -50,49 +50,71 @@ stages: - bash: | set -uo pipefail - mkdir -p test-results - OVERALL_RC=0 + mkdir -p test-results logs + + run_unit() { + local V="$1" + local LOG="logs/unit-${V}.log" + { + echo "=== Unit tests · Python ${V} (parallel) ===" + local PY="python${V}" + if ! command -v "$PY" >/dev/null; then + echo "ERROR: $PY not on PATH" + exit 1 + fi + "$PY" -m venv ".venv-unit-${V}" + # shellcheck disable=SC1090 + source ".venv-unit-${V}/bin/activate" + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-azurepipelines + pytest -vv \ + --junitxml="test-results/junit-unit-${V}.xml" \ + --ignore=tests/test_e2e.py \ + --ignore=tests/test_e2e_manual.py \ + --ignore=tests/test_fmi_e2e.py + local RC=$? + deactivate + exit $RC + } >"$LOG" 2>&1 + } - for V in 3.9 3.10 3.11 3.12 3.13 3.14; do - echo "============================================================" - echo " Unit tests · Python ${V}" - echo "============================================================" + VERSIONS=(3.9 3.10 3.11 3.12 3.13 3.14) + PIDS=() + for V in "${VERSIONS[@]}"; do + run_unit "$V" & + PIDS+=($!) + done - PY="python${V}" - if ! command -v "$PY" >/dev/null; then - echo "##vso[task.logissue type=error]${PY} not on PATH" + OVERALL_RC=0 + declare -A RESULTS + for i in "${!VERSIONS[@]}"; do + V="${VERSIONS[$i]}" + PID="${PIDS[$i]}" + if wait "$PID"; then + RESULTS[$V]="PASS" + else + RESULTS[$V]="FAIL (rc=$?)" OVERALL_RC=1 - continue fi + done - "$PY" -m venv ".venv-${V}" - # shellcheck disable=SC1090 - source ".venv-${V}/bin/activate" - - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pytest pytest-azurepipelines - - set -o pipefail - pytest -vv \ - --junitxml="test-results/junit-unit-${V}.xml" \ - --ignore=tests/test_e2e.py \ - --ignore=tests/test_e2e_manual.py \ - --ignore=tests/test_fmi_e2e.py \ - 2>&1 | tee "test-results/pytest-unit-${V}.log" - RC=$? - set +o pipefail - - deactivate + for V in "${VERSIONS[@]}"; do + echo "" + echo "############################################################" + echo "# Unit tests · Python ${V} · ${RESULTS[$V]}" + echo "############################################################" + cat "logs/unit-${V}.log" + done - if [ $RC -ne 0 ]; then - echo "Python ${V}: unit pytest exited ${RC}" - OVERALL_RC=1 - fi + echo "" + echo "=== Summary ===" + for V in "${VERSIONS[@]}"; do + echo " Python ${V}: ${RESULTS[$V]}" done exit $OVERALL_RC - displayName: 'Run pytest (unit, all Python versions)' + displayName: 'Run pytest (unit, all Python versions in parallel)' - task: PublishTestResults@2 displayName: 'Publish JUnit test results' @@ -115,7 +137,7 @@ stages: condition: succeeded() jobs: - job: Pytest - displayName: 'pytest (E2E, all Python versions)' + displayName: 'E2E tests' pool: vmImage: ubuntu-22.04 timeoutInMinutes: 60 @@ -163,47 +185,69 @@ stages: - bash: | set -uo pipefail - mkdir -p test-results - OVERALL_RC=0 + mkdir -p test-results logs + + run_e2e() { + local V="$1" + local LOG="logs/e2e-${V}.log" + { + echo "=== E2E tests · Python ${V} (parallel) ===" + local PY="python${V}" + if ! command -v "$PY" >/dev/null; then + echo "ERROR: $PY not on PATH" + exit 1 + fi + "$PY" -m venv ".venv-e2e-${V}" + # shellcheck disable=SC1090 + source ".venv-e2e-${V}/bin/activate" + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-azurepipelines + pytest -vv \ + --junitxml="test-results/junit-e2e-${V}.xml" \ + tests/test_e2e.py tests/test_fmi_e2e.py + local RC=$? + deactivate + exit $RC + } >"$LOG" 2>&1 + } - for V in 3.9 3.10 3.11 3.12 3.13 3.14; do - echo "============================================================" - echo " E2E tests · Python ${V}" - echo "============================================================" + VERSIONS=(3.9 3.10 3.11 3.12 3.13 3.14) + PIDS=() + for V in "${VERSIONS[@]}"; do + run_e2e "$V" & + PIDS+=($!) + done - PY="python${V}" - if ! command -v "$PY" >/dev/null; then - echo "##vso[task.logissue type=error]${PY} not on PATH" + OVERALL_RC=0 + declare -A RESULTS + for i in "${!VERSIONS[@]}"; do + V="${VERSIONS[$i]}" + PID="${PIDS[$i]}" + if wait "$PID"; then + RESULTS[$V]="PASS" + else + RESULTS[$V]="FAIL (rc=$?)" OVERALL_RC=1 - continue fi + done - "$PY" -m venv ".venv-${V}" - # shellcheck disable=SC1090 - source ".venv-${V}/bin/activate" - - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pytest pytest-azurepipelines - - set -o pipefail - pytest -vv \ - --junitxml="test-results/junit-e2e-${V}.xml" \ - tests/test_e2e.py tests/test_fmi_e2e.py \ - 2>&1 | tee "test-results/pytest-e2e-${V}.log" - RC=$? - set +o pipefail - - deactivate + for V in "${VERSIONS[@]}"; do + echo "" + echo "############################################################" + echo "# E2E tests · Python ${V} · ${RESULTS[$V]}" + echo "############################################################" + cat "logs/e2e-${V}.log" + done - if [ $RC -ne 0 ]; then - echo "Python ${V}: e2e pytest exited ${RC}" - OVERALL_RC=1 - fi + echo "" + echo "=== Summary ===" + for V in "${VERSIONS[@]}"; do + echo " Python ${V}: ${RESULTS[$V]}" done exit $OVERALL_RC - displayName: 'Run pytest (E2E, all Python versions)' + displayName: 'Run pytest (E2E, all Python versions in parallel)' env: LAB_APP_CLIENT_CERT_PFX_PATH: $(LAB_APP_CLIENT_CERT_PFX_PATH) From ca155138b8cfd301655bf1e0e28859e51051aa3f Mon Sep 17 00:00:00 2001 From: Nilesh Choudhary Date: Thu, 14 May 2026 11:50:42 +0100 Subject: [PATCH 8/8] python version parallel tests --- .Pipelines/pipeline-release.yml | 168 ++++++++++++++++++++ .Pipelines/pipeline-unit-tests.yml | 241 ++++++++--------------------- 2 files changed, 235 insertions(+), 174 deletions(-) create mode 100644 .Pipelines/pipeline-release.yml diff --git a/.Pipelines/pipeline-release.yml b/.Pipelines/pipeline-release.yml new file mode 100644 index 00000000..e0848daa --- /dev/null +++ b/.Pipelines/pipeline-release.yml @@ -0,0 +1,168 @@ +# ADO release pipeline for the msal Python package. +# +# Mirrors the GitHub CD flow in .github/workflows/python-package.yml (the `cd:` job): +# - push to a `release-*` branch → publish to TestPyPI +# - push of a tag (e.g. `1.36.0`) → publish to PyPI (with manual approval) +# +# Secrets are fetched at run time from Key Vault `msidlabs` via the +# `AuthSdkResourceManager` service connection (same pattern as the lab cert in +# pipeline-unit-tests.yml). Required Key Vault secrets: +# - TestPyPiApiToken → TestPyPI API token (starts with `pypi-`) +# - PyPiApiToken → PyPI API token (starts with `pypi-`) +# +# Required ADO setup before first run: +# 1. Add the two secrets above to the `msidlabs` Key Vault. +# 2. Authorize the `AuthSdkResourceManager` SC for this pipeline (one-time +# "Permit" prompt on the first run). +# 3. Create an Environment named `msal-py-pypi` with a required approver +# (ADO → Pipelines → Environments → New environment → Approvals and checks). + +trigger: + branches: + include: + - release-* + tags: + include: + - '*' + +pr: none + +variables: + - name: pythonBuildVersion + value: '3.12' + +stages: + +# ───────────────────────────────────────────────────────────────────────────── +# Stage 1 · Build sdist + wheel, publish as a pipeline artifact. +# ───────────────────────────────────────────────────────────────────────────── +- stage: Build + displayName: 'Build' + jobs: + - job: BuildDist + displayName: 'sdist + wheel' + pool: + vmImage: ubuntu-22.04 + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python $(pythonBuildVersion)' + inputs: + versionSpec: $(pythonBuildVersion) + + - bash: | + set -euo pipefail + python -m pip install --upgrade pip build twine + python -m build --sdist --wheel --outdir dist/ . + python -m twine check dist/* + ls -la dist/ + displayName: 'Build + twine check' + + - task: PublishPipelineArtifact@1 + displayName: 'Publish dist/ as pipeline artifact' + inputs: + targetPath: dist/ + artifact: python-dist + +# ───────────────────────────────────────────────────────────────────────────── +# Stage 2a · Publish to TestPyPI — runs on push to a release-* branch. +# No approval gate (matches GitHub flow today). +# ───────────────────────────────────────────────────────────────────────────── +- stage: PublishTestPyPI + displayName: 'Publish to TestPyPI' + dependsOn: Build + condition: | + and( + succeeded(), + startsWith(variables['Build.SourceBranch'], 'refs/heads/release-') + ) + jobs: + - job: Upload + displayName: 'twine upload → test.pypi.org' + pool: + vmImage: ubuntu-22.04 + steps: + - checkout: none + + - task: DownloadPipelineArtifact@2 + displayName: 'Download dist/ artifact' + inputs: + artifactName: python-dist + targetPath: dist/ + + - task: UsePythonVersion@0 + displayName: 'Use Python $(pythonBuildVersion)' + inputs: + versionSpec: $(pythonBuildVersion) + + - task: AzureKeyVault@2 + displayName: 'Fetch TestPyPI API token from Key Vault' + inputs: + azureSubscription: 'AuthSdkResourceManager' + KeyVaultName: 'msidlabs' + SecretsFilter: 'TestPyPiApiToken' + RunAsPreJob: false + + - bash: | + set -euo pipefail + python -m pip install --upgrade pip twine + python -m twine upload \ + --repository-url https://test.pypi.org/legacy/ \ + --username __token__ \ + --password "$TWINE_PASSWORD" \ + --skip-existing \ + dist/* + displayName: 'twine upload → test.pypi.org' + env: + TWINE_PASSWORD: $(TestPyPiApiToken) + +# ───────────────────────────────────────────────────────────────────────────── +# Stage 2b · Publish to PyPI — runs on push of a tag. +# Manual approval enforced via the `msal-py-pypi` Environment. +# ───────────────────────────────────────────────────────────────────────────── +- stage: PublishPyPI + displayName: 'Publish to PyPI' + dependsOn: Build + condition: | + and( + succeeded(), + startsWith(variables['Build.SourceBranch'], 'refs/tags/') + ) + jobs: + - deployment: Upload + displayName: 'twine upload → pypi.org' + pool: + vmImage: ubuntu-22.04 + environment: 'msal-py-pypi' + strategy: + runOnce: + deploy: + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download dist/ artifact' + inputs: + artifactName: python-dist + targetPath: dist/ + + - task: UsePythonVersion@0 + displayName: 'Use Python $(pythonBuildVersion)' + inputs: + versionSpec: $(pythonBuildVersion) + + - task: AzureKeyVault@2 + displayName: 'Fetch PyPI API token from Key Vault' + inputs: + azureSubscription: 'AuthSdkResourceManager' + KeyVaultName: 'msidlabs' + SecretsFilter: 'PyPiApiToken' + RunAsPreJob: false + + - bash: | + set -euo pipefail + python -m pip install --upgrade pip twine + python -m twine upload \ + --username __token__ \ + --password "$TWINE_PASSWORD" \ + dist/* + displayName: 'twine upload → pypi.org' + env: + TWINE_PASSWORD: $(PyPiApiToken) diff --git a/.Pipelines/pipeline-unit-tests.yml b/.Pipelines/pipeline-unit-tests.yml index 6b35926f..187ce1d5 100644 --- a/.Pipelines/pipeline-unit-tests.yml +++ b/.Pipelines/pipeline-unit-tests.yml @@ -1,7 +1,7 @@ # ADO pipeline for the msal Python test suite. -# Two stages → two GitHub checks (Unit tests, E2E tests). Each stage is one -# job that fans out the Python matrix as parallel bash background processes -# so wall-clock time ≈ slowest single Python version, not the sum. +# Two stages → two GitHub stage checks (Unit test, E2E tests). Each stage +# fans out the Python matrix as separate jobs so every Python version runs +# on its own agent in parallel with its own timeout. trigger: branches: @@ -18,118 +18,65 @@ pr: stages: # ───────────────────────────────────────────────────────────────────────────── -# Stage 1 · Unit tests — no Key Vault, no service connection. +# Stage 1 · Unit test — no Key Vault, no service connection. # ───────────────────────────────────────────────────────────────────────────── - stage: UnitTests displayName: 'Unit test' jobs: - job: Pytest - displayName: 'Unit test' + displayName: 'pytest' pool: vmImage: ubuntu-22.04 timeoutInMinutes: 30 + strategy: + matrix: + Python39: { python.version: '3.9' } + Python310: { python.version: '3.10' } + Python311: { python.version: '3.11' } + Python312: { python.version: '3.12' } + Python313: { python.version: '3.13' } + Python314: { python.version: '3.14' } + maxParallel: 6 steps: - task: UsePythonVersion@0 - displayName: 'Use Python 3.9' - inputs: { versionSpec: '3.9' } - - task: UsePythonVersion@0 - displayName: 'Use Python 3.10' - inputs: { versionSpec: '3.10' } - - task: UsePythonVersion@0 - displayName: 'Use Python 3.11' - inputs: { versionSpec: '3.11' } - - task: UsePythonVersion@0 - displayName: 'Use Python 3.12' - inputs: { versionSpec: '3.12' } - - task: UsePythonVersion@0 - displayName: 'Use Python 3.13' - inputs: { versionSpec: '3.13' } - - task: UsePythonVersion@0 - displayName: 'Use Python 3.14' - inputs: { versionSpec: '3.14' } + displayName: 'Use Python $(python.version)' + inputs: + versionSpec: '$(python.version)' - bash: | - set -uo pipefail - mkdir -p test-results logs - - run_unit() { - local V="$1" - local LOG="logs/unit-${V}.log" - { - echo "=== Unit tests · Python ${V} (parallel) ===" - local PY="python${V}" - if ! command -v "$PY" >/dev/null; then - echo "ERROR: $PY not on PATH" - exit 1 - fi - "$PY" -m venv ".venv-unit-${V}" - # shellcheck disable=SC1090 - source ".venv-unit-${V}/bin/activate" - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pytest pytest-azurepipelines - pytest -vv \ - --junitxml="test-results/junit-unit-${V}.xml" \ - --ignore=tests/test_e2e.py \ - --ignore=tests/test_e2e_manual.py \ - --ignore=tests/test_fmi_e2e.py - local RC=$? - deactivate - exit $RC - } >"$LOG" 2>&1 - } - - VERSIONS=(3.9 3.10 3.11 3.12 3.13 3.14) - PIDS=() - for V in "${VERSIONS[@]}"; do - run_unit "$V" & - PIDS+=($!) - done - - OVERALL_RC=0 - declare -A RESULTS - for i in "${!VERSIONS[@]}"; do - V="${VERSIONS[$i]}" - PID="${PIDS[$i]}" - if wait "$PID"; then - RESULTS[$V]="PASS" - else - RESULTS[$V]="FAIL (rc=$?)" - OVERALL_RC=1 - fi - done - - for V in "${VERSIONS[@]}"; do - echo "" - echo "############################################################" - echo "# Unit tests · Python ${V} · ${RESULTS[$V]}" - echo "############################################################" - cat "logs/unit-${V}.log" - done - - echo "" - echo "=== Summary ===" - for V in "${VERSIONS[@]}"; do - echo " Python ${V}: ${RESULTS[$V]}" - done + set -euo pipefail + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-azurepipelines + displayName: 'Install Python dependencies' - exit $OVERALL_RC - displayName: 'Run pytest (unit, all Python versions in parallel)' + - bash: | + set -o pipefail + mkdir -p test-results + pytest -vv \ + --junitxml=test-results/junit-unit.xml \ + --ignore=tests/test_e2e.py \ + --ignore=tests/test_e2e_manual.py \ + --ignore=tests/test_fmi_e2e.py \ + 2>&1 | tee test-results/pytest-unit.log + displayName: 'Run pytest (unit)' - task: PublishTestResults@2 displayName: 'Publish JUnit test results' condition: succeededOrFailed() inputs: testResultsFormat: 'JUnit' - testResultsFiles: 'test-results/junit-unit-*.xml' + testResultsFiles: 'test-results/junit-unit.xml' failTaskOnFailedTests: true - testRunTitle: 'Unit tests' - mergeTestResults: true + testRunTitle: 'Unit · Python $(python.version)' # ───────────────────────────────────────────────────────────────────────────── # Stage 2 · E2E tests — runs only if unit tests pass. Fetches the MSID Lab # certificate from Key Vault (mirrors MSAL.NET's # build/template-install-keyvault-secrets.yaml). +# Skipped on forked PRs — service connections / Key Vault are not +# available to forks. E2E tests self-skip when LAB_APP_CLIENT_CERT_PFX_PATH +# is unset (matches the pattern in template-pipeline-stages.yml). # ───────────────────────────────────────────────────────────────────────────── - stage: E2ETests displayName: 'E2E tests' @@ -137,32 +84,28 @@ stages: condition: succeeded() jobs: - job: Pytest - displayName: 'E2E tests' + displayName: 'pytest' pool: vmImage: ubuntu-22.04 timeoutInMinutes: 60 + strategy: + matrix: + Python39: { python.version: '3.9' } + Python310: { python.version: '3.10' } + Python311: { python.version: '3.11' } + Python312: { python.version: '3.12' } + Python313: { python.version: '3.13' } + Python314: { python.version: '3.14' } + maxParallel: 6 steps: - task: UsePythonVersion@0 - displayName: 'Use Python 3.9' - inputs: { versionSpec: '3.9' } - - task: UsePythonVersion@0 - displayName: 'Use Python 3.10' - inputs: { versionSpec: '3.10' } - - task: UsePythonVersion@0 - displayName: 'Use Python 3.11' - inputs: { versionSpec: '3.11' } - - task: UsePythonVersion@0 - displayName: 'Use Python 3.12' - inputs: { versionSpec: '3.12' } - - task: UsePythonVersion@0 - displayName: 'Use Python 3.13' - inputs: { versionSpec: '3.13' } - - task: UsePythonVersion@0 - displayName: 'Use Python 3.14' - inputs: { versionSpec: '3.14' } + displayName: 'Use Python $(python.version)' + inputs: + versionSpec: '$(python.version)' - task: AzureKeyVault@2 displayName: 'Fetch MSID Lab certificate from Key Vault' + condition: ne(variables['System.PullRequest.IsFork'], 'True') inputs: azureSubscription: 'AuthSdkResourceManager' KeyVaultName: 'msidlabs' @@ -180,74 +123,25 @@ stages: echo "##vso[task.setvariable variable=LAB_APP_CLIENT_CERT_PFX_PATH]$CERT_PATH" echo "Lab cert written to: $CERT_PATH ($(wc -c < "$CERT_PATH") bytes)" displayName: 'Decode lab certificate to PFX' + condition: ne(variables['System.PullRequest.IsFork'], 'True') env: LAB_AUTH_B64: $(LabAuth) - bash: | - set -uo pipefail - mkdir -p test-results logs - - run_e2e() { - local V="$1" - local LOG="logs/e2e-${V}.log" - { - echo "=== E2E tests · Python ${V} (parallel) ===" - local PY="python${V}" - if ! command -v "$PY" >/dev/null; then - echo "ERROR: $PY not on PATH" - exit 1 - fi - "$PY" -m venv ".venv-e2e-${V}" - # shellcheck disable=SC1090 - source ".venv-e2e-${V}/bin/activate" - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install pytest pytest-azurepipelines - pytest -vv \ - --junitxml="test-results/junit-e2e-${V}.xml" \ - tests/test_e2e.py tests/test_fmi_e2e.py - local RC=$? - deactivate - exit $RC - } >"$LOG" 2>&1 - } - - VERSIONS=(3.9 3.10 3.11 3.12 3.13 3.14) - PIDS=() - for V in "${VERSIONS[@]}"; do - run_e2e "$V" & - PIDS+=($!) - done - - OVERALL_RC=0 - declare -A RESULTS - for i in "${!VERSIONS[@]}"; do - V="${VERSIONS[$i]}" - PID="${PIDS[$i]}" - if wait "$PID"; then - RESULTS[$V]="PASS" - else - RESULTS[$V]="FAIL (rc=$?)" - OVERALL_RC=1 - fi - done - - for V in "${VERSIONS[@]}"; do - echo "" - echo "############################################################" - echo "# E2E tests · Python ${V} · ${RESULTS[$V]}" - echo "############################################################" - cat "logs/e2e-${V}.log" - done - - echo "" - echo "=== Summary ===" - for V in "${VERSIONS[@]}"; do - echo " Python ${V}: ${RESULTS[$V]}" - done + set -euo pipefail + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-azurepipelines + displayName: 'Install Python dependencies' - exit $OVERALL_RC - displayName: 'Run pytest (E2E, all Python versions in parallel)' + - bash: | + set -o pipefail + mkdir -p test-results + pytest -vv \ + --junitxml=test-results/junit-e2e.xml \ + tests/test_e2e.py tests/test_fmi_e2e.py \ + 2>&1 | tee test-results/pytest-e2e.log + displayName: 'Run pytest (E2E)' env: LAB_APP_CLIENT_CERT_PFX_PATH: $(LAB_APP_CLIENT_CERT_PFX_PATH) @@ -256,10 +150,9 @@ stages: condition: succeededOrFailed() inputs: testResultsFormat: 'JUnit' - testResultsFiles: 'test-results/junit-e2e-*.xml' + testResultsFiles: 'test-results/junit-e2e.xml' failTaskOnFailedTests: true - testRunTitle: 'E2E tests' - mergeTestResults: true + testRunTitle: 'E2E · Python $(python.version)' - bash: rm -f "$(Agent.TempDirectory)/lab-auth.pfx" displayName: 'Remove lab certificate from agent'