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
65 changes: 0 additions & 65 deletions .azure-devops/merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,71 +48,6 @@ jobs:
- template: templates/setup-python.yml
- template: templates/build-publish-azure-cli-test-sdk.yml

- job: "run_unit_tests_ubuntu"
dependsOn:
[
"build_and_publish_azure_iot_cli_ext",
"build_and_publish_azure_cli_test_sdk",
]
pool:
vmImage: ${{ parameters.linuxImage }}
strategy:
matrix:
Python310:
python.version: "3.10"
Python311:
python.version: "3.11"
Python312:
python.version: "3.12"
Python313:
python.version: "3.13"

steps:
- bash: sudo rm -R -f /usr/local/lib/azureExtensionDir

- template: templates/run-tests-parallel.yml
parameters:
pythonVersion: "$(python.version)"
runUnitTests: true
runIntTests: false

- job: "run_unit_tests_macOs"
dependsOn:
[
"build_and_publish_azure_iot_cli_ext",
"build_and_publish_azure_cli_test_sdk",
]
pool:
vmImage: "macOS-15"

steps:
- template: templates/run-tests-parallel.yml
parameters:
pythonVersion: "3.12"
runUnitTests: true
runIntTests: false

- job: "run_unit_tests_windows"
dependsOn:
[
"build_and_publish_azure_iot_cli_ext",
"build_and_publish_azure_cli_test_sdk",
]
pool:
vmImage: ${{ parameters.windowsImage }}

steps:
- task: PowerShell@2
inputs:
targetType: "inline"
script: 'ren "C:\Program Files\Common Files\AzureCliExtensionDirectory" "C:\Program Files\Common Files\AzureCliExtensionDirectory1"'

- template: templates/run-tests-parallel.yml
parameters:
pythonVersion: "3.12"
runUnitTests: true
runIntTests: false

- job: "run_style_check"
dependsOn:
[
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/ci_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,21 @@ concurrency:
on:
pull_request:
push:
branches:
- dev
- preview
- 1.1.0-preview
- "release/**"
tags:
- "**"
workflow_dispatch:
jobs:
build:
uses: ./.github/workflows/ci_build.yml
test:
uses: ./.github/workflows/tox.yml
with:
pr-matrix: ${{ github.event_name == 'pull_request' }}
linter:
needs: [build]
uses: ./.github/workflows/azdev_linter.yml
Expand Down
101 changes: 92 additions & 9 deletions .github/workflows/int_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,10 @@ jobs:
echo "|---------|--------|--------|--------------|---------|" >> $GITHUB_STEP_SUMMARY
echo "$matrix" | jq -r '.[] | "| \(.service) | \(.python) | \(.region) | \(.arm_endpoint) | \(.timeout) min |"' >> $GITHUB_STEP_SUMMARY

unit-test:
name: "Linter and unit tests"
lint:
name: "Linter"
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
Expand All @@ -289,23 +291,104 @@ jobs:
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.13"
cache: "pip" # Cache pip downloads to avoid re-downloading packages across runs
cache: "pip"

- name: "Setup test suite"
- name: "Run linter"
run: |
python -m pip install tox
tox r -vv --notest
python -m tox r -e lint -vv

- name: "Run linter and unit tests"
run: tox r --skip-pkg-install
unit-shards:
name: "Unit tests (shard ${{ matrix.shard }}/4)"
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 25
strategy:
fail-fast: false
max-parallel: 4
matrix:
shard: [1, 2, 3, 4]
steps:
- name: "Checkout source"
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0

- name: "Upload coverage report"
- name: "Setup Python 3.13"
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.13"
cache: "pip"

- name: "Run balanced serial unit shard"
env:
SHARD: ${{ matrix.shard }}
UNIT_RUN_ID: "github:${{ github.repository }}:${{ github.run_id }}"
UNIT_COMMIT: ${{ github.sha }}
UNIT_ATTEMPT: ${{ github.run_attempt }}
RESULTS: ${{ github.workspace }}/unit-result
COVERAGE_FILE: ${{ github.workspace }}/unit-result/coverage.dat
run: |
python -m pip install tox
python -m tox r -e python-azcur-unit -vv -- \
-p azext_iot.tests._unit_shard_plugin --unit-shard "$SHARD" \
--unit-shard-output "$RESULTS" --junitxml "$RESULTS/junit.xml"

- name: "Upload unit shard evidence"
if: ${{ always() }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: unit-shard-${{ matrix.shard }}-${{ github.run_attempt }}
path: unit-result/
if-no-files-found: error
retention-days: 30

unit-test:
name: "Linter and unit test gate"
needs: [lint, unit-shards]
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: "Checkout source"
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0

- name: "Setup Python 3.13"
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.13"

- name: "Download unit shard evidence"
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
pattern: unit-shard-*
path: unit-history

- name: "Require complete unit coverage and combine results"
env:
UNIT_RUN_ID: "github:${{ github.repository }}:${{ github.run_id }}"
UNIT_COMMIT: ${{ github.sha }}
run: |
python -m pip install coverage
python azext_iot/tests/_unit_shards.py --history unit-history --output unit-coverage

- name: "Upload unit timing and completeness reports"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: unit-summary-${{ github.run_attempt }}
path: |
unit-coverage/summary.json
unit-coverage/timings.json
if-no-files-found: error
retention-days: 30

- name: "Upload coverage report"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: coverage-unit
overwrite: true
path: ./.coverage
path: unit-coverage/.coverage
if-no-files-found: error
include-hidden-files: true
retention-days: 30

Expand Down
89 changes: 86 additions & 3 deletions .github/workflows/tox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ name: Tox tests
on:
workflow_call:
inputs:
pr-matrix:
description: Test Python 3.13 on every OS and Python 3.10 on Ubuntu only
type: boolean
required: false
default: false
continue-on-error:
type: boolean
required: false
Expand All @@ -19,7 +24,7 @@ permissions:

jobs:
tox:
name: Unit test ${{ matrix.py }} - ${{ matrix.os }}
name: Unit test ${{ matrix.py }} - ${{ matrix.os }} (shard ${{ matrix.shard }}/4)
continue-on-error: ${{ inputs.continue-on-error }}
runs-on: ${{ matrix.os }}
strategy:
Expand All @@ -34,6 +39,8 @@ jobs:
- "3.12"
- "3.11"
- "3.10"
shard: [1, 2, 3, 4]
exclude: ${{ fromJSON(inputs.pr-matrix && '[{"py":"3.12"},{"py":"3.11"},{"os":"windows-2025","py":"3.10"},{"os":"macos-15-intel","py":"3.10"}]' || '[]') }}
steps:
- name: Setup python ${{ matrix.py }}
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
Expand All @@ -49,9 +56,85 @@ jobs:
- name: Setup test suite
run: |
python -m pip install tox
tox r -vv --notest
python -m tox r -e python-azcur-unit -vv --notest
- name: Run test suite
run: tox r --skip-pkg-install
env:
UNIT_RUN_ID: "github:${{ github.repository }}:${{ github.run_id }}:tox:${{ matrix.os }}:py${{ matrix.py }}"
UNIT_COMMIT: ${{ github.sha }}
UNIT_ATTEMPT: ${{ github.run_attempt }}
COVERAGE_FILE: ${{ github.workspace }}/unit-result/coverage.dat
run: >-
python -m tox r -e python-azcur-unit --skip-pkg-install --
-p azext_iot.tests._unit_shard_plugin --unit-shard ${{ matrix.shard }}
--unit-shard-output unit-result --junitxml unit-result/junit.xml
- name: Upload unit shard evidence
if: ${{ always() }}
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: tox-unit-${{ matrix.os }}-py${{ matrix.py }}-${{ matrix.shard }}-${{ github.run_attempt }}
path: unit-result/
if-no-files-found: error
retention-days: 30
- name: Run lint once per OS and Python
if: ${{ matrix.shard == 1 }}
run: python -m tox r -e lint -vv

unit-gate:
name: Unit test ${{ matrix.py }} - ${{ matrix.os }}
needs: tox
if: ${{ always() }}
continue-on-error: ${{ inputs.continue-on-error }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-24.04
- windows-2025
- macos-15-intel
py:
- "3.13"
- "3.12"
- "3.11"
- "3.10"
exclude: ${{ fromJSON(inputs.pr-matrix && '[{"py":"3.12"},{"py":"3.11"},{"os":"windows-2025","py":"3.10"},{"os":"macos-15-intel","py":"3.10"}]' || '[]') }}
steps:
- name: Require successful unit shards and lint
if: ${{ needs.tox.result != 'success' }}
run: |
echo "::error::Unit shards or lint did not succeed (${{ needs.tox.result }})."
exit 1
- name: Setup python ${{ matrix.py }}
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: ${{ matrix.py }}
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- name: Download this matrix combination's unit evidence
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
pattern: tox-unit-${{ matrix.os }}-py${{ matrix.py }}-*
path: unit-history
- name: Install coverage
run: python -m pip install coverage
- name: Verify complete unit execution and combine coverage
env:
UNIT_RUN_ID: "github:${{ github.repository }}:${{ github.run_id }}:tox:${{ matrix.os }}:py${{ matrix.py }}"
UNIT_COMMIT: ${{ github.sha }}
run: >-
python azext_iot/tests/_unit_shards.py --history unit-history --output unit-coverage
--prefix tox-unit-${{ matrix.os }}-py${{ matrix.py }}
- name: Generate coverage reports
env:
COVERAGE_FILE: ${{ github.workspace }}/unit-coverage/.coverage
run: python -m coverage report && python -m coverage html && python -m coverage json
- name: Upload verified unit results
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: tox-summary-${{ matrix.os }}-py${{ matrix.py }}-${{ github.run_attempt }}
path: unit-coverage/
include-hidden-files: true
if-no-files-found: error
retention-days: 30
- name: Upload coverage report
if: ${{ matrix.os == 'ubuntu-24.04' && matrix.py == '3.13' }}
continue-on-error: true
Expand Down
40 changes: 40 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,46 @@ Execute the following command to run the IoT Hub unit tests:

`pytest azext_iot/tests/iothub/ -k "_unit.py"`

#### Parallel CI unit tests

The GitHub integration precheck uses four balanced unit-test shards on
Linux/Python 3.13. PR CI uses the same runner on Python 3.13 across Ubuntu,
Windows, and macOS, plus Python 3.10 on Ubuntu: four combinations and 16 unit jobs.
GitHub branch/tag pushes, manual runs, and release workflows retain all three operating
systems and Python 3.10-3.13: twelve combinations and 48 unit jobs. The reusable
workflow defaults to the full matrix; only the CI caller's `pull_request` event
enables `pr-matrix`. Each combination runs all unit tests across four shards,
with lint once per combination and a coverage gate for each. Each shard uses
serial pytest, not xdist, and new unit files are included automatically.
Shards must use the same Python major/minor version; runner patch versions may differ.
PR aggregate gates retain the `test / Unit test <python> - <os>` names required
by branch rules and fail if any shard or lint run fails.

Feature-branch updates run GitHub CI through `pull_request`, without a duplicate
push-triggered matrix. Push CI remains enabled for `dev`, `preview`,
`1.1.0-preview`, `release/**`, and tags; manual dispatch is unchanged.

Azure Pipelines Merge (pipeline 11) no longer repeats the unit suite. It retains
wheel and software-manifest generation, the Azure CLI test SDK build, style and
command-table lint, and Credential Scan. The four GitHub PR combinations are
the PR unit gate; the former additional ADO unit combinations and tests against
the unreleased CLI test SDK are no longer part of Merge validation. Full-matrix
GitHub runs and other integration/unit pipelines are unchanged.

Integration starts only after lint, all shards, and the `unit-test` gate pass.
The gate checks complete, exactly-once case coverage and combines coverage into
`coverage-unit`. Native reruns can reuse successful shards from the same run and
commit, but incomplete newer attempts cannot fall back to older results.

Shard results are published as `unit-shard-<number>-<attempt>`.
PR CI uses `tox-unit-<os>-py<version>-<number>-<attempt>` to keep each combination
separate from the others and from integration prechecks.
Integration summaries use `unit-summary-<attempt>`; PR summaries use
`tox-summary-<os>-py<version>-<attempt>`. Both include the completeness summary
and `timings.json`; use those timings to maintain `azext_iot/tests/unit_test_durations.json`.
The shared runner accepts CI-neutral run, commit, and attempt inputs so Azure
Pipelines can use the same implementation.

### Integration Tests

Integration tests are run against Azure resources and depend on environment variables.
Expand Down
Loading
Loading