This is an automated email from the ASF dual-hosted git repository.

vatsrahul1001 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 6c9a2c34523d025cac153d1cff654274844df2ef
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 30 13:21:30 2026 +0100

    [v3-4-test] Size CI test jobs on RunsOn runners to the parallelism they can 
use (#75833) (#75992)
    
    * Size CI test jobs on RunsOn runners to the parallelism they can use
    
    Jobs that run tests with --run-in-parallel start one serial pytest per test 
type, so they never use more CPUs than they have test types. On RunsOn, 
splitting core test types into two jobs only doubles the jobs: each half still 
lasts as long as its slowest test type, and the out-of-memory risk the split 
guards against on public runners does not apply to the larger RunsOn runners. 
The lowest-dependencies provider jobs left their 32 CPUs partly idle, and the 
integration-core and system t [...]
    
    Generated-by: Claude Opus 5
    
    * Run provider tests one provider per test type on RunsOn runners
    
    Grouping all providers but the long-running ones into one test type left 
the provider jobs waiting on that group's single serial pytest while the other 
CPUs idled. The Non-DB jobs run every provider in one xdist pytest, where 
splitting test types only adds entries, so they get the providers unsplit.
    
    Generated-by: Claude Opus 5
    
    * Give docker on RunsOn runners enough address pools for parallel test runs
    
    Each test type of a parallel test run is a docker compose project with its 
own network. Docker's default address pools fit about 30 networks, so provider 
jobs running a test type per CPU on 32-CPU and larger runners failed with "all 
predefined address pools have been fully subnetted".
    
    Generated-by: Claude Opus 5
    
    * Split the longest-running provider tests into several test types on RunsOn
    
    Once every provider runs as its own test type, a job lasts as long as its 
slowest provider, and amazon, google and standard each take far longer than the 
rest as a single serial pytest. Splitting them lets the large RunsOn runners 
work on their parts at once. Lowest-dependencies jobs are sized by test-type 
count, so a change touching a few providers keeps one job.
    
    Generated-by: Claude Opus 5
    
    * Keep split provider test types out of compose project and artifact names
    
    Docker Compose project names and GitHub artifact names reject the colon 
that names a part of a split provider.
    
    Generated-by: Claude Opus 5
    
    * Skip split provider test types whose provider is ignored
    
    A split provider part selects a folder inside the provider's tests, and 
pytest runs an explicitly selected path even when its provider folder is 
ignored, so a skipped or excluded provider's parts would still run.
    
    Generated-by: Claude Opus 5
    
    * Fit RunsOn runner sizes to the work of each job and record their CPU and 
memory use
    
    Several jobs ran a single process, or a minute or two of work, on 32-CPU or 
8-CPU runners, and fourteen Helm unit test jobs each spent more time preparing 
the image than running their tests. Charting each job's CPU and memory use lets 
the size classes be set from measurements rather than from how the jobs are 
expected to behave.
    
    Generated-by: Claude Opus 5
    
    * Skip ignored test selections by the paths the test type selects
    
    The check for an empty run treated the first remaining argument, or any 
path-like argument, as a test selection, so option values such as the "8" of 
"-n 8" or a path passed to --rootdir decided whether tests ran. It now compares 
the ignored folders with the paths the test type itself selects, and passes 
paths the user names explicitly to pytest as they are.
    
    Generated-by: Claude Opus 5
    
    * Reject split provider test types where all test types share one pytest
    
    Under xdist every test type runs in one pytest process, where the --ignore 
arguments of a provider's other part would silently be dropped or would remove 
its sibling parts, so the wrong tests would run.
    
    Generated-by: Claude Opus 5
    
    * Count only test folders as the selections of a test type
    
    Test suites such as All-Postgres add pytest options with values, which were 
taken for selected test paths, so a run whose every test folder was ignored 
still started pytest with nothing selected.
    
    Generated-by: Claude Opus 5
    
    * Run only its own test type in each worker of a parallel test run
    
    Each worker kept the whole list of test types, so with --skip-db-tests 
every worker selected the folders of all of them.
    
    Generated-by: Claude Opus 5
    
    * Raise the AIO limit on RunsOn runners for parallel MySQL tests
    
    A few dozen MySQL servers running at once exhaust the default 
fs.aio-max-nr, and InnoDB then fails with "io_setup() failed with EAGAIN".
    
    Generated-by: Claude Opus 5
    
    * Keep the provider compatibility tests on the grouped provider test types
    
    Each test type of a compatibility test installs an Airflow version and the 
providers before running, so listing every provider as its own test type 
multiplied those installs and the jobs ran out of time.
    (cherry picked from commit 083b82d7c4d6fe3864027d0de42fdba48bb1ad8b)
    
    
    Generated-by: Claude Opus 5
    
    Co-authored-by: Jarek Potiuk <[email protected]>
---
 .../actions/prepare_breeze_and_image/action.yml    |   5 +
 .github/actions/runs-on-cost-summary/action.yml    |   4 +-
 .github/workflows/additional-prod-image-tests.yml  |   8 +-
 .github/workflows/basic-tests.yml                  |   4 +-
 .github/workflows/ci-amd.yml                       |  30 ++--
 .github/workflows/ci-arm.yml                       |  30 ++--
 .github/workflows/finalize-tests.yml               |   2 +-
 .github/workflows/helm-tests.yml                   |   2 +-
 .github/workflows/integration-system-tests.yml     |   2 +-
 .github/workflows/special-tests.yml                |  22 +--
 Dockerfile.ci                                      |   2 +
 dev/breeze/doc/ci/04_selective_checks.md           |  41 ++++-
 .../airflow_breeze/commands/testing_commands.py    |  29 +++-
 dev/breeze/src/airflow_breeze/global_constants.py  |  36 +++++
 .../src/airflow_breeze/params/shell_params.py      |   2 +
 .../airflow_breeze/utils/ci_minutes_estimate.py    |   6 +-
 dev/breeze/src/airflow_breeze/utils/run_tests.py   | 101 ++++++++++---
 .../src/airflow_breeze/utils/selective_checks.py   |  86 +++++++++--
 .../tests/test_pytest_args_for_test_types.py       |  65 +++++++-
 dev/breeze/tests/test_run_test_args.py             |  93 ++++++++++++
 dev/breeze/tests/test_selective_checks.py          | 168 +++++++++++++++++++++
 dev/breeze/tests/test_shell_params.py              |  15 +-
 scripts/ci/configure_runner_for_parallel_tests.sh  |  46 ++++++
 scripts/docker/entrypoint_ci.sh                    |   2 +
 24 files changed, 707 insertions(+), 94 deletions(-)

diff --git a/.github/actions/prepare_breeze_and_image/action.yml 
b/.github/actions/prepare_breeze_and_image/action.yml
index 263db0346d4..7cad3d17033 100644
--- a/.github/actions/prepare_breeze_and_image/action.yml
+++ b/.github/actions/prepare_breeze_and_image/action.yml
@@ -71,6 +71,11 @@ runs:
         else
           ./scripts/tools/free_up_disk_space.sh
         fi
+    - name: "Configure runner for parallel tests"
+      shell: bash
+      run: ./scripts/ci/configure_runner_for_parallel_tests.sh
+      # Only self-hosted runners are large enough to run more test types at 
once than the default pools fit.
+      if: runner.environment == 'self-hosted'
     - name: "Install Breeze"
       uses: ./.github/actions/breeze
       id: breeze
diff --git a/.github/actions/runs-on-cost-summary/action.yml 
b/.github/actions/runs-on-cost-summary/action.yml
index fd0d2007f10..d31493ae746 100644
--- a/.github/actions/runs-on-cost-summary/action.yml
+++ b/.github/actions/runs-on-cost-summary/action.yml
@@ -19,7 +19,7 @@
 # Puts the job's EC2 cost next to the GitHub-hosted price in the job summary. 
Used by every job
 # that can run on RunsOn: through the breeze action, or directly by jobs that 
do not install Breeze.
 name: 'RunsOn cost summary'
-description: 'Add the EC2 cost of a RunsOn job to its summary; a no-op 
elsewhere.'
+description: 'Add the EC2 cost and CPU and memory use of a RunsOn job to its 
summary; a no-op elsewhere.'
 runs:
   using: 'composite'
   steps:
@@ -28,3 +28,5 @@ runs:
       uses: runs-on/action@17b59092eb37ebfdbb034460892887c09100eaa3  # v2.5.0
       with:
         show_costs: summary
+        # Charts the job's CPU and memory use in its summary, to size its 
runner class from measurements.
+        metrics: cpu,memory
diff --git a/.github/workflows/additional-prod-image-tests.yml 
b/.github/workflows/additional-prod-image-tests.yml
index a0109fb65a6..47657cbe678 100644
--- a/.github/workflows/additional-prod-image-tests.yml
+++ b/.github/workflows/additional-prod-image-tests.yml
@@ -201,7 +201,7 @@ jobs:
     timeout-minutes: 60
     name: "Docker Compose quick start with PROD image verifying"
     if: inputs.run-generic-prod-image-tests == 'true'
-    runs-on: ${{ format(fromJSON(inputs.runners)[0], '32+64', '128+256') }}
+    runs-on: ${{ format(fromJSON(inputs.runners)[0], '8+16', '32+64') }}
     env:
       PYTHON_MAJOR_MINOR_VERSION: "${{ inputs.default-python-version }}"
       GITHUB_REPOSITORY: ${{ github.repository }}
@@ -331,7 +331,7 @@ jobs:
     uses: ./.github/workflows/airflow-e2e-tests.yml
     with:
       workflow-name: "Event driven e2e test"
-      runners: ${{ format(inputs.runners, '32+64', '128+256') }}
+      runners: ${{ format(inputs.runners, '8+16', '32+64') }}
       platform: ${{ inputs.platform }}
       default-python-version: "${{ inputs.default-python-version }}"
       use-uv: ${{ inputs.use-uv }}
@@ -359,7 +359,7 @@ jobs:
     uses: ./.github/workflows/airflow-e2e-tests.yml
     with:
       workflow-name: "Go SDK e2e test"
-      runners: ${{ format(inputs.runners, '32+64', '128+256') }}
+      runners: ${{ format(inputs.runners, '8+16', '32+64') }}
       platform: ${{ inputs.platform }}
       default-python-version: "${{ inputs.default-python-version }}"
       use-uv: ${{ inputs.use-uv }}
@@ -373,7 +373,7 @@ jobs:
     uses: ./.github/workflows/airflow-e2e-tests.yml
     with:
       workflow-name: "TypeScript SDK e2e test"
-      runners: ${{ format(inputs.runners, '32+64', '128+256') }}
+      runners: ${{ format(inputs.runners, '8+16', '32+64') }}
       platform: ${{ inputs.platform }}
       default-python-version: "${{ inputs.default-python-version }}"
       use-uv: ${{ inputs.use-uv }}
diff --git a/.github/workflows/basic-tests.yml 
b/.github/workflows/basic-tests.yml
index d1b84c5a156..86f59d5f1ac 100644
--- a/.github/workflows/basic-tests.yml
+++ b/.github/workflows/basic-tests.yml
@@ -148,7 +148,7 @@ jobs:
       # Without a pin uv picks the runner's system Python, which can be older 
than Airflow supports.
       UV_PYTHON: "${{ inputs.default-python-version }}"
       SHARED_DISTRIBUTIONS: ${{ 
join(fromJSON(inputs.shared-distributions-as-json), ' ') }}
-    runs-on: ${{ format(fromJSON(inputs.runners)[0], '8+16', '32+64') }}
+    runs-on: ${{ format(fromJSON(inputs.runners)[0], '4+8', '16+32') }}
     steps:
       - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
         uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # 
v7.0.1
@@ -255,7 +255,7 @@ jobs:
   check-translation-completness:
     timeout-minutes: 15
     name: "Check translation completeness"
-    runs-on: ${{ format(fromJSON(inputs.runners)[0], '32+64', '128+256') }}
+    runs-on: ${{ format(fromJSON(inputs.runners)[0], '4+8', '16+32') }}
     steps:
       - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
         uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # 
v7.0.1
diff --git a/.github/workflows/ci-amd.yml b/.github/workflows/ci-amd.yml
index 98dc1726b8f..8fb2157fcd7 100644
--- a/.github/workflows/ci-amd.yml
+++ b/.github/workflows/ci-amd.yml
@@ -320,6 +320,10 @@ jobs:
         ${{ 
steps.selective-checks.outputs.providers-compatibility-tests-matrix }}
       providers-test-types-list-as-strings-in-json: >-
         ${{ 
steps.selective-checks.outputs.providers-test-types-list-as-strings-in-json }}
+      providers-split-test-types-list-as-strings-in-json: >-
+        ${{ 
steps.selective-checks.outputs.providers-split-test-types-list-as-strings-in-json
 }}
+      providers-xdist-test-types-list-as-strings-in-json: >-
+        ${{ 
steps.selective-checks.outputs.providers-xdist-test-types-list-as-strings-in-json
 }}
       pull-request-labels: ${{ steps.source-run-info.outputs.pr-labels }}
       python-versions-list-as-string: ${{ 
steps.selective-checks.outputs.python-versions-list-as-string }}
       python-versions: ${{ steps.selective-checks.outputs.python-versions }}
@@ -798,7 +802,7 @@ jobs:
       contents: read
       packages: read
     with:
-      runners: ${{ format(needs.build-info.outputs.in-memory-runner-type, 
'8+16', '32+128') }}
+      runners: ${{ format(needs.build-info.outputs.in-memory-runner-type, 
'32+64', '128+256') }}
       platform: ${{ needs.build-info.outputs.platform }}
       backend: "postgres"
       test-name: "Postgres"
@@ -809,7 +813,7 @@ jobs:
       excluded-providers-as-string: ${{ 
needs.build-info.outputs.excluded-providers-as-string }}
       excludes: ${{ needs.build-info.outputs.postgres-exclude }}
       test-types-as-strings-in-json: >
-        ${{ 
needs.build-info.outputs.providers-test-types-list-as-strings-in-json }}
+        ${{ 
needs.build-info.outputs.providers-split-test-types-list-as-strings-in-json }}
       include-success-outputs: ${{ 
needs.build-info.outputs.include-success-outputs }}
       run-migration-tests: "true"
       run-coverage: ${{ needs.build-info.outputs.run-coverage }}
@@ -857,7 +861,7 @@ jobs:
       contents: read
       packages: read
     with:
-      runners: ${{ format(needs.build-info.outputs.in-memory-runner-type, 
'8+16', '32+128') }}
+      runners: ${{ format(needs.build-info.outputs.in-memory-runner-type, 
'32+64', '128+256') }}
       platform: ${{ needs.build-info.outputs.platform }}
       backend: "mysql"
       test-name: "MySQL"
@@ -868,7 +872,7 @@ jobs:
       excluded-providers-as-string: ${{ 
needs.build-info.outputs.excluded-providers-as-string }}
       excludes: ${{ needs.build-info.outputs.mysql-exclude }}
       test-types-as-strings-in-json: >
-        ${{ 
needs.build-info.outputs.providers-test-types-list-as-strings-in-json }}
+        ${{ 
needs.build-info.outputs.providers-split-test-types-list-as-strings-in-json }}
       include-success-outputs: ${{ 
needs.build-info.outputs.include-success-outputs }}
       run-coverage: ${{ needs.build-info.outputs.run-coverage }}
       run-migration-tests: "true"
@@ -917,7 +921,7 @@ jobs:
       contents: read
       packages: read
     with:
-      runners: ${{ format(needs.build-info.outputs.in-memory-runner-type, 
'8+16', '32+128') }}
+      runners: ${{ format(needs.build-info.outputs.in-memory-runner-type, 
'32+64', '128+256') }}
       platform: ${{ needs.build-info.outputs.platform }}
       backend: "sqlite"
       test-name: "Sqlite"
@@ -930,7 +934,7 @@ jobs:
       excluded-providers-as-string: ${{ 
needs.build-info.outputs.excluded-providers-as-string }}
       excludes: ${{ needs.build-info.outputs.sqlite-exclude }}
       test-types-as-strings-in-json: >
-        ${{ 
needs.build-info.outputs.providers-test-types-list-as-strings-in-json }}
+        ${{ 
needs.build-info.outputs.providers-split-test-types-list-as-strings-in-json }}
       include-success-outputs: ${{ 
needs.build-info.outputs.include-success-outputs }}
       run-coverage: ${{ needs.build-info.outputs.run-coverage }}
       run-migration-tests: "true"
@@ -991,7 +995,7 @@ jobs:
       excluded-providers-as-string: ${{ 
needs.build-info.outputs.excluded-providers-as-string }}
       excludes: ${{ needs.build-info.outputs.sqlite-exclude }}
       test-types-as-strings-in-json: >
-        ${{ 
needs.build-info.outputs.providers-test-types-list-as-strings-in-json }}
+        ${{ 
needs.build-info.outputs.providers-xdist-test-types-list-as-strings-in-json }}
       include-success-outputs: ${{ 
needs.build-info.outputs.include-success-outputs }}
       run-coverage: ${{ needs.build-info.outputs.run-coverage }}
       debug-resources: ${{ needs.build-info.outputs.debug-resources }}
@@ -1021,6 +1025,8 @@ jobs:
         ${{ needs.build-info.outputs.core-test-types-list-as-strings-in-json }}
       providers-test-types-list-as-strings-in-json: >
         ${{ 
needs.build-info.outputs.providers-test-types-list-as-strings-in-json }}
+      providers-split-test-types-list-as-strings-in-json: >
+        ${{ 
needs.build-info.outputs.providers-split-test-types-list-as-strings-in-json }}
       run-coverage: ${{ needs.build-info.outputs.run-coverage }}
       default-python-version: "${{ 
needs.build-info.outputs.default-python-version }}"
       python-versions: ${{ needs.build-info.outputs.python-versions }}
@@ -1207,7 +1213,7 @@ jobs:
       contents: read
       packages: read
     with:
-      runners: ${{ format(needs.build-info.outputs.runner-type, '32+64', 
'128+256') }}
+      runners: ${{ format(needs.build-info.outputs.runner-type, '8+16', 
'32+64') }}
       platform: ${{ needs.build-info.outputs.platform }}
       default-python-version: "${{ 
needs.build-info.outputs.default-python-version }}"
       python-versions: ${{ needs.build-info.outputs.python-versions }}
@@ -1292,7 +1298,7 @@ jobs:
   tests-ts-sdk:
     name: "TypeScript SDK tests"
     needs: [build-info]
-    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'8+16', '32+64') }}
+    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'4+8', '16+32') }}
     timeout-minutes: 15
     permissions:
       contents: read
@@ -1328,7 +1334,7 @@ jobs:
   tests-java-sdk:
     name: "Java SDK tests"
     needs: [build-info]
-    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'32+64', '128+256') }}
+    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'8+16', '32+64') }}
     timeout-minutes: 15
     permissions:
       contents: read
@@ -1414,7 +1420,7 @@ jobs:
   build-java-sdk-docs:
     name: "Java SDK docs"
     needs: [build-info]
-    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'8+16', '32+64') }}
+    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'4+8', '16+32') }}
     timeout-minutes: 30
     permissions:
       contents: read
@@ -1830,7 +1836,7 @@ jobs:
       - tests-special
       - tests-with-lowest-direct-resolution-core
       - tests-with-lowest-direct-resolution-providers
-    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'8+16', '32+64') }}
+    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'4+8', '16+32') }}
     if: needs.build-info.outputs.run-unit-tests == 'true'
     steps:
       - name: "Cleanup repo"
diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml
index 8aa56eab63a..3fdfaa4efee 100644
--- a/.github/workflows/ci-arm.yml
+++ b/.github/workflows/ci-arm.yml
@@ -201,6 +201,10 @@ jobs:
         ${{ 
steps.selective-checks.outputs.providers-compatibility-tests-matrix }}
       providers-test-types-list-as-strings-in-json: >-
         ${{ 
steps.selective-checks.outputs.providers-test-types-list-as-strings-in-json }}
+      providers-split-test-types-list-as-strings-in-json: >-
+        ${{ 
steps.selective-checks.outputs.providers-split-test-types-list-as-strings-in-json
 }}
+      providers-xdist-test-types-list-as-strings-in-json: >-
+        ${{ 
steps.selective-checks.outputs.providers-xdist-test-types-list-as-strings-in-json
 }}
       pull-request-labels: ${{ steps.source-run-info.outputs.pr-labels }}
       python-versions-list-as-string: ${{ 
steps.selective-checks.outputs.python-versions-list-as-string }}
       python-versions: ${{ steps.selective-checks.outputs.python-versions }}
@@ -679,7 +683,7 @@ jobs:
       contents: read
       packages: read
     with:
-      runners: ${{ format(needs.build-info.outputs.in-memory-runner-type, 
'8+16', '32+128') }}
+      runners: ${{ format(needs.build-info.outputs.in-memory-runner-type, 
'32+64', '128+256') }}
       platform: ${{ needs.build-info.outputs.platform }}
       backend: "postgres"
       test-name: "Postgres"
@@ -690,7 +694,7 @@ jobs:
       excluded-providers-as-string: ${{ 
needs.build-info.outputs.excluded-providers-as-string }}
       excludes: ${{ needs.build-info.outputs.postgres-exclude }}
       test-types-as-strings-in-json: >
-        ${{ 
needs.build-info.outputs.providers-test-types-list-as-strings-in-json }}
+        ${{ 
needs.build-info.outputs.providers-split-test-types-list-as-strings-in-json }}
       include-success-outputs: ${{ 
needs.build-info.outputs.include-success-outputs }}
       run-migration-tests: "true"
       run-coverage: ${{ needs.build-info.outputs.run-coverage }}
@@ -738,7 +742,7 @@ jobs:
       contents: read
       packages: read
     with:
-      runners: ${{ format(needs.build-info.outputs.in-memory-runner-type, 
'8+16', '32+128') }}
+      runners: ${{ format(needs.build-info.outputs.in-memory-runner-type, 
'32+64', '128+256') }}
       platform: ${{ needs.build-info.outputs.platform }}
       backend: "mysql"
       test-name: "MySQL"
@@ -749,7 +753,7 @@ jobs:
       excluded-providers-as-string: ${{ 
needs.build-info.outputs.excluded-providers-as-string }}
       excludes: ${{ needs.build-info.outputs.mysql-exclude }}
       test-types-as-strings-in-json: >
-        ${{ 
needs.build-info.outputs.providers-test-types-list-as-strings-in-json }}
+        ${{ 
needs.build-info.outputs.providers-split-test-types-list-as-strings-in-json }}
       include-success-outputs: ${{ 
needs.build-info.outputs.include-success-outputs }}
       run-coverage: ${{ needs.build-info.outputs.run-coverage }}
       run-migration-tests: "true"
@@ -798,7 +802,7 @@ jobs:
       contents: read
       packages: read
     with:
-      runners: ${{ format(needs.build-info.outputs.in-memory-runner-type, 
'8+16', '32+128') }}
+      runners: ${{ format(needs.build-info.outputs.in-memory-runner-type, 
'32+64', '128+256') }}
       platform: ${{ needs.build-info.outputs.platform }}
       backend: "sqlite"
       test-name: "Sqlite"
@@ -811,7 +815,7 @@ jobs:
       excluded-providers-as-string: ${{ 
needs.build-info.outputs.excluded-providers-as-string }}
       excludes: ${{ needs.build-info.outputs.sqlite-exclude }}
       test-types-as-strings-in-json: >
-        ${{ 
needs.build-info.outputs.providers-test-types-list-as-strings-in-json }}
+        ${{ 
needs.build-info.outputs.providers-split-test-types-list-as-strings-in-json }}
       include-success-outputs: ${{ 
needs.build-info.outputs.include-success-outputs }}
       run-coverage: ${{ needs.build-info.outputs.run-coverage }}
       run-migration-tests: "true"
@@ -872,7 +876,7 @@ jobs:
       excluded-providers-as-string: ${{ 
needs.build-info.outputs.excluded-providers-as-string }}
       excludes: ${{ needs.build-info.outputs.sqlite-exclude }}
       test-types-as-strings-in-json: >
-        ${{ 
needs.build-info.outputs.providers-test-types-list-as-strings-in-json }}
+        ${{ 
needs.build-info.outputs.providers-xdist-test-types-list-as-strings-in-json }}
       include-success-outputs: ${{ 
needs.build-info.outputs.include-success-outputs }}
       run-coverage: ${{ needs.build-info.outputs.run-coverage }}
       debug-resources: ${{ needs.build-info.outputs.debug-resources }}
@@ -902,6 +906,8 @@ jobs:
         ${{ needs.build-info.outputs.core-test-types-list-as-strings-in-json }}
       providers-test-types-list-as-strings-in-json: >
         ${{ 
needs.build-info.outputs.providers-test-types-list-as-strings-in-json }}
+      providers-split-test-types-list-as-strings-in-json: >
+        ${{ 
needs.build-info.outputs.providers-split-test-types-list-as-strings-in-json }}
       run-coverage: ${{ needs.build-info.outputs.run-coverage }}
       default-python-version: "${{ 
needs.build-info.outputs.default-python-version }}"
       python-versions: ${{ needs.build-info.outputs.python-versions }}
@@ -1088,7 +1094,7 @@ jobs:
       contents: read
       packages: read
     with:
-      runners: ${{ format(needs.build-info.outputs.runner-type, '32+64', 
'128+256') }}
+      runners: ${{ format(needs.build-info.outputs.runner-type, '8+16', 
'32+64') }}
       platform: ${{ needs.build-info.outputs.platform }}
       default-python-version: "${{ 
needs.build-info.outputs.default-python-version }}"
       python-versions: ${{ needs.build-info.outputs.python-versions }}
@@ -1173,7 +1179,7 @@ jobs:
   tests-ts-sdk:
     name: "TypeScript SDK tests"
     needs: [build-info]
-    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'8+16', '32+64') }}
+    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'4+8', '16+32') }}
     timeout-minutes: 15
     permissions:
       contents: read
@@ -1209,7 +1215,7 @@ jobs:
   tests-java-sdk:
     name: "Java SDK tests"
     needs: [build-info]
-    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'32+64', '128+256') }}
+    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'8+16', '32+64') }}
     timeout-minutes: 15
     permissions:
       contents: read
@@ -1295,7 +1301,7 @@ jobs:
   build-java-sdk-docs:
     name: "Java SDK docs"
     needs: [build-info]
-    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'8+16', '32+64') }}
+    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'4+8', '16+32') }}
     timeout-minutes: 30
     permissions:
       contents: read
@@ -1709,7 +1715,7 @@ jobs:
       - tests-special
       - tests-with-lowest-direct-resolution-core
       - tests-with-lowest-direct-resolution-providers
-    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'8+16', '32+64') }}
+    runs-on: ${{ format(fromJSON(needs.build-info.outputs.runner-type)[0], 
'4+8', '16+32') }}
     if: needs.build-info.outputs.run-unit-tests == 'true'
     steps:
       - name: "Cleanup repo"
diff --git a/.github/workflows/finalize-tests.yml 
b/.github/workflows/finalize-tests.yml
index 9715a8cfc61..d9d5c463b15 100644
--- a/.github/workflows/finalize-tests.yml
+++ b/.github/workflows/finalize-tests.yml
@@ -80,7 +80,7 @@ permissions:
   contents: read
 jobs:
   update-constraints:
-    runs-on: ${{ format(fromJSON(inputs.runners)[0], '8+16', '32+64') }}
+    runs-on: ${{ format(fromJSON(inputs.runners)[0], '4+8', '16+32') }}
     timeout-minutes: 80
     name: "Update constraints"
     permissions:
diff --git a/.github/workflows/helm-tests.yml b/.github/workflows/helm-tests.yml
index cf140b98c62..f8000a7c4dd 100644
--- a/.github/workflows/helm-tests.yml
+++ b/.github/workflows/helm-tests.yml
@@ -98,7 +98,7 @@ jobs:
   tests-helm-release:
     timeout-minutes: 80
     name: "Release Helm"
-    runs-on: ${{ format(fromJSON(inputs.runners)[0], '8+16', '32+64') }}
+    runs-on: ${{ format(fromJSON(inputs.runners)[0], '4+8', '16+32') }}
     env:
       PYTHON_MAJOR_MINOR_VERSION: "${{inputs.default-python-version}}"
       GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/integration-system-tests.yml 
b/.github/workflows/integration-system-tests.yml
index 8b24223a55f..e61e4070e0c 100644
--- a/.github/workflows/integration-system-tests.yml
+++ b/.github/workflows/integration-system-tests.yml
@@ -75,7 +75,7 @@ jobs:
     timeout-minutes: 30
     if: inputs.testable-core-integrations != '[]'
     name: "Integration core ${{ matrix.integration }}"
-    runs-on: ${{ format(fromJSON(inputs.runners)[0], '32+64', '128+256') }}
+    runs-on: ${{ format(fromJSON(inputs.runners)[0], '8+16', '32+64') }}
     strategy:
       fail-fast: false
       max-parallel: 20
diff --git a/.github/workflows/special-tests.yml 
b/.github/workflows/special-tests.yml
index 09658246df9..1b47ac5259a 100644
--- a/.github/workflows/special-tests.yml
+++ b/.github/workflows/special-tests.yml
@@ -44,6 +44,10 @@ on:  # yamllint disable-line rule:truthy
         description: "The list of providers test types to run separated by 
spaces"
         required: true
         type: string
+      providers-split-test-types-list-as-strings-in-json:
+        description: "The providers test types for the jobs running them in 
parallel in the CI image"
+        required: true
+        type: string
       run-coverage:
         description: "Whether to run coverage or not (true/false)"
         required: true
@@ -123,7 +127,7 @@ jobs:
       contents: read
       packages: read
     with:
-      runners: ${{ format(inputs.in-memory-runners, '8+16', '32+128') }}
+      runners: ${{ format(inputs.in-memory-runners, '32+64', '128+256') }}
       platform: ${{ inputs.platform }}
       downgrade-sqlalchemy: "true"
       test-name: "MinSQLAlchemy-Postgres"
@@ -134,7 +138,7 @@ jobs:
       backend-versions: "['${{ inputs.default-postgres-version }}']"
       excluded-providers-as-string: ${{ inputs.excluded-providers-as-string }}
       excludes: "[]"
-      test-types-as-strings-in-json: ${{ 
inputs.providers-test-types-list-as-strings-in-json }}
+      test-types-as-strings-in-json: ${{ 
inputs.providers-split-test-types-list-as-strings-in-json }}
       run-coverage: ${{ inputs.run-coverage }}
       debug-resources: ${{ inputs.debug-resources }}
       skip-providers-tests: ${{ inputs.skip-providers-tests }}
@@ -173,7 +177,7 @@ jobs:
       contents: read
       packages: read
     with:
-      runners: ${{ format(inputs.in-memory-runners, '8+16', '32+128') }}
+      runners: ${{ format(inputs.in-memory-runners, '32+64', '128+256') }}
       platform: ${{ inputs.platform }}
       upgrade-sqlalchemy: "true"
       test-name: "LatestSQLAlchemy-Postgres"
@@ -184,7 +188,7 @@ jobs:
       backend-versions: "['${{ inputs.default-postgres-version }}']"
       excluded-providers-as-string: ${{ inputs.excluded-providers-as-string }}
       excludes: "[]"
-      test-types-as-strings-in-json: ${{ 
inputs.providers-test-types-list-as-strings-in-json }}
+      test-types-as-strings-in-json: ${{ 
inputs.providers-split-test-types-list-as-strings-in-json }}
       run-coverage: ${{ inputs.run-coverage }}
       debug-resources: ${{ inputs.debug-resources }}
       skip-providers-tests: ${{ inputs.skip-providers-tests }}
@@ -224,7 +228,7 @@ jobs:
       contents: read
       packages: read
     with:
-      runners: ${{ format(inputs.in-memory-runners, '8+16', '32+128') }}
+      runners: ${{ format(inputs.in-memory-runners, '32+64', '128+256') }}
       platform: ${{ inputs.platform }}
       upgrade-boto: "true"
       test-name: "LatestBoto-Postgres"
@@ -235,7 +239,7 @@ jobs:
       backend-versions: "['${{ inputs.default-postgres-version }}']"
       excluded-providers-as-string: ${{ inputs.excluded-providers-as-string }}
       excludes: "[]"
-      test-types-as-strings-in-json: ${{ 
inputs.providers-test-types-list-as-strings-in-json }}
+      test-types-as-strings-in-json: ${{ 
inputs.providers-split-test-types-list-as-strings-in-json }}
       include-success-outputs: ${{ inputs.include-success-outputs }}
       run-coverage: ${{ inputs.run-coverage }}
       debug-resources: ${{ inputs.debug-resources }}
@@ -276,7 +280,7 @@ jobs:
       contents: read
       packages: read
     with:
-      runners: ${{ format(inputs.in-memory-runners, '8+16', '32+128') }}
+      runners: ${{ format(inputs.in-memory-runners, '32+64', '128+256') }}
       platform: ${{ inputs.platform }}
       downgrade-pendulum: "true"
       test-name: "Pendulum2-Postgres"
@@ -287,7 +291,7 @@ jobs:
       backend-versions: "['${{ inputs.default-postgres-version }}']"
       excluded-providers-as-string: ${{ inputs.excluded-providers-as-string }}
       excludes: "[]"
-      test-types-as-strings-in-json: ${{ 
inputs.providers-test-types-list-as-strings-in-json }}
+      test-types-as-strings-in-json: ${{ 
inputs.providers-split-test-types-list-as-strings-in-json }}
       include-success-outputs: ${{ inputs.include-success-outputs }}
       run-coverage: ${{ inputs.run-coverage }}
       debug-resources: ${{ inputs.debug-resources }}
@@ -352,7 +356,7 @@ jobs:
       contents: read
       packages: read
     with:
-      runners: ${{ format(inputs.runners, '32+64', '128+256') }}
+      runners: ${{ format(inputs.runners, '8+16', '32+64') }}
       platform: ${{ inputs.platform }}
       test-name: "SystemTest"
       test-scope: "System"
diff --git a/Dockerfile.ci b/Dockerfile.ci
index a16046aee4b..33b6970c14c 100644
--- a/Dockerfile.ci
+++ b/Dockerfile.ci
@@ -1779,6 +1779,8 @@ function check_force_lowest_dependencies() {
         local provider_id
         # shellcheck disable=SC2001
         provider_id=$(echo "${TEST_TYPE}" | sed 's/Providers\[\(.*\)\]/\1/')
+        # A split provider's test type names one part of its tests: 
Providers[<id>:<part>]
+        provider_id=${provider_id%%:*}
         echo
         echo "${COLOR_BLUE}Forcing dependencies to lowest versions for 
provider: ${provider_id}${COLOR_RESET}"
         echo
diff --git a/dev/breeze/doc/ci/04_selective_checks.md 
b/dev/breeze/doc/ci/04_selective_checks.md
index 051fd3d5699..409e2cba52e 100644
--- a/dev/breeze/doc/ci/04_selective_checks.md
+++ b/dev/breeze/doc/ci/04_selective_checks.md
@@ -671,7 +671,7 @@ GitHub Actions to pass the list of parameters to a command 
to execute
 | all-versions                                            | If set to true, 
then all python, k8s, DB versions are used for tests.                           
        | false                                    |      |
 | basic-checks-only                                       | Whether to run all 
static checks ("false") or only basic set of static checks ("true")             
     | false                                    |      |
 | ci-image-build                                          | Whether CI image 
build is needed                                                                 
       | true                                     |      |
-| core-test-types-list-as-strings-in-json                 | Which test types 
should be run for unit tests for core                                           
       | API Always Providers                     | *    |
+| core-test-types-list-as-strings-in-json                 | Which test types 
should be run for unit tests for core (2 slices; 1 on RunsOn)                   
       | API Always Providers                     | *    |
 | debug-resources                                         | Whether resources 
usage should be printed during parallel job execution ("true"/ "false")         
      | false                                    |      |
 | default-branch                                          | Which branch is 
default for the build ("main" for main branch, "v2-4-test" for 2.4 line etc.)   
        | main                                     |      |
 | default-constraints-branch                              | Which branch is 
default for the build ("constraints-main" for main branch, "constraints-2-4" 
etc.)      | constraints-main                         |      |
@@ -691,11 +691,11 @@ GitHub Actions to pass the list of parameters to a 
command to execute
 | generated-dependencies-changed                          | Whether generated 
dependencies have changed ("true"/"false")                                      
      | false                                    |      |
 | has-migrations                                          | Whether the PR has 
migrations ("true"/"false")                                                     
     | false                                    |      |
 | hatch-build-changed                                     | When hatch 
build.py changed in the PR.                                                     
             | false                                    |      |
-| helm-test-packages-list-as-string                       | List of helm 
packages to test as JSON array                                                  
           | \["airflow_aux", "airflow_core"\]        | *    |
+| helm-test-packages                                      | List of helm 
packages to test as JSON array (`["All"]` on RunsOn: one job runs them all)     
         | \["airflow_aux", "airflow_core"\]        | *    |
 | helm-version                                            | Which Helm version 
to use for tests                                                                
     | v3.15.3                                  |      |
 | in-memory-runner-type                                   | Like runner-type, 
but the RunsOn template puts docker in RAM (`extras=tmpfs`) unless disabled     
      | \["ubuntu-22.04"\]                       |      |
 | include-success-outputs                                 | Whether to include 
outputs of successful parallel tests ("true"/"false")                           
     | false                                    |      |
-| individual-providers-test-types-list-as-strings-in-json | Which test types 
should be run for unit tests for providers (individually listed)                
       | Providers[\amazon\] Providers\[google\]  | *    |
+| individual-providers-test-types-list-as-strings-in-json | Which test types 
should be run for unit tests for providers (individually listed; 5 slices, 3 on 
RunsOn)| Providers[\amazon\] Providers\[google\]  | *    |
 | is-committer-build                                      | Whether the build 
is triggered by a committer                                                     
      | false                                    |      |
 | is-legacy-ui-api-labeled                                | Whether the PR is 
labeled as legacy UI/API                                                        
      | false                                    |      |
 | java-sdk-version                                        | JDK version used 
to build the lang-SDK Java artifacts natively in CI                             
        | 17                                       |      |
@@ -713,6 +713,8 @@ GitHub Actions to pass the list of parameters to a command 
to execute
 | prod-image-build                                        | Whether PROD image 
build is needed                                                                 
     | true                                     |      |
 | providers-compatibility-tests-matrix                    | Matrix of 
providers compatibility tests: (python_version, airflow_version, 
removed_providers). Excludes unbuilt python versions and the entry whose 
airflow_version equals the current branch version (unreleased on a vX-Y-test 
branch until GA) | \[{}\]                                   |      |
 | providers-test-types-list-as-strings-in-json            | Which test types 
should be run for unit tests for providers                                      
       | Providers Providers\[-google\]           | *    |
+| providers-split-test-types-list-as-strings-in-json      | Provider test 
types for the jobs running them in parallel in the CI image (one per provider 
on RunsOn)  | Providers Providers\[-google\]           | *    |
+| providers-xdist-test-types-list-as-strings-in-json      | Which test types 
the Non-DB (xdist) provider tests run, with no provider split out               
       | Providers                                | *    |
 | pyproject-toml-changed                                  | When 
pyproject.toml changed in the PR.                                               
                   | false                                    |      |
 | python-versions                                         | List of python 
versions to use for that build                                                  
         | \['3.11'\]                               |      |
 | python-versions-list-as-string                          | Which versions of 
MySQL to use for tests as space-separated string                                
      | 3.11                                     | *    |
@@ -788,12 +790,43 @@ picks its size class with 
`format(fromJSON(inputs.runners)[0], <cpu>, <ram>)` (`
 `'8+16', '32+64'` or `'32+64', '128+256'`), or, for a single-job reusable 
workflow, its caller passes
 `format(<runners>, <cpu>, <ram>)`. Each class is a range up to twice its size, 
so Spot can come from
 the larger instance types too; xdist follows the host's CPUs, so a larger 
runner runs more test workers.
+Jobs using `--run-in-parallel` (the DB, lowest-dependencies and special tests) 
run one serial pytest per
+test type instead, so they never use more CPUs than they have test types, and 
jobs running a single pytest
+(integration and system tests) use one. The size classes follow that: the 
xdist jobs and every job
+running provider tests in parallel take `'32+64', '128+256'`, the others 
`'8+16', '32+64'` or smaller:
+a job running one process (an end-to-end test against one Airflow stack, a 
Gradle test run) takes
+`'8+16', '32+64'`, and a job with a minute or two of work takes `'4+8', 
'16+32'`. On RunsOn the Helm unit
+tests run all their packages in one job per Kubernetes version, since each 
package takes about a minute
+under xdist against two to three minutes of preparing the image.
+On RunsOn `providers-split-test-types-list-as-strings-in-json`, which the 
provider jobs running in the CI
+image read, lists each provider as its own test type, in one job, instead of 
the long-running providers
+plus one group of all the others, so the job is not left waiting on that 
group. The provider
+compatibility tests keep the grouped 
`providers-test-types-list-as-strings-in-json`, because each of their
+test types installs an Airflow version first. The Non-DB xdist jobs read
+`providers-xdist-test-types-list-as-strings-in-json` instead, which never 
splits the providers: xdist runs
+them all in one pytest anyway.
+The matrix slices follow it too. On public runners the core test types are 
split into
+`NUMBER_OF_CORE_SLICES` (2) jobs, to keep the Non-DB xdist collection inside 
the runner's memory, and
+the individually listed providers into `NUMBER_OF_LOW_DEP_SLICES` (5). On 
RunsOn the core test types
+stay in one job (`NUMBER_OF_CORE_SLICES_ON_RUNS_ON`), because a core DB job 
lasts as long as its
+slowest test type whichever slice it is in, and the providers are split into 
jobs of about 40 test types
+(`LOW_DEP_TEST_TYPES_PER_JOB_ON_RUNS_ON`), which the heaviest-first ordering 
keeps the 32 CPUs busy with:
+3 jobs for all providers, one for a change touching a few. On RunsOn the 
longest-running providers
+(`PROVIDER_TEST_SPLITS`: amazon, google and standard) are also split into 
several test types such as
+`Providers[amazon:hooks]`, with `Providers[amazon:other]` running the rest of 
the provider's tests, so
+none of them leaves the job waiting on one long serial pytest.
 `spot=co` takes Spot from the pools with the most capacity regardless of 
price, which is cheaper than
 the on-demand launch and whole-job rerun a capacity miss or interruption costs;
 on a public label the same call returns the label unchanged. Steps that only 
make sense on RunsOn are
 gated on `runner.environment == 'self-hosted'`:
 
-* The EC2 cost summary of each job.
+* The EC2 cost summary of each job, with charts of the job's CPU and memory 
use (`metrics: cpu,memory` of
+  `runs-on/action`), the measurements to size a job's runner class by.
+* The runner settings for parallel tests 
(`scripts/ci/configure_runner_for_parallel_tests.sh`). Every test
+  type of a parallel run is a docker compose project with a network and a 
database of its own. Docker's
+  default address pools fit about 30 networks, fewer than a RunsOn runner runs 
test types at once, so the
+  script splits the same range into `/24` subnets; and it raises 
`fs.aio-max-nr`, which a few dozen MySQL
+  servers exhaust.
 * The image stashes (`.github/actions/stash-restore` / `stash-save`). On 
RunsOn the image tarballs go to
   the stack's S3 cache bucket, named by their sha256, and only the digest is 
stashed as a GitHub artifact.
   Any RunsOn job can write to the bucket, so a tarball is used only if it 
matches the digest stashed by
diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands.py 
b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
index 279f05d2a86..8934b40cbf6 100644
--- a/dev/breeze/src/airflow_breeze/commands/testing_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
@@ -90,9 +90,11 @@ from airflow_breeze.global_constants import (
     ALL_TEST_TYPE,
     ALLOWED_KUBERNETES_VERSIONS,
     ALLOWED_TEST_TYPE_CHOICES,
+    PROVIDER_TEST_SPLITS,
     TESTCONTAINERS_IMAGES_BY_PROVIDER,
     GroupOfTests,
     all_selective_core_test_types,
+    get_provider_split_test_types,
     providers_test_type,
 )
 from airflow_breeze.params.build_prod_params import BuildProdParams
@@ -127,6 +129,8 @@ from airflow_breeze.utils.run_tests import (
     are_all_test_paths_excluded,
     file_name_from_test_type,
     generate_args_for_pytest,
+    get_selected_test_paths,
+    is_path_under_any,
     is_provider_selected_in_test_types,
     run_docker_compose_tests,
 )
@@ -270,13 +274,21 @@ def _run_test(
         integration=shell_params.integration,
     )
     pytest_args.extend(extra_pytest_args)
-    # Skip "FOLDER" in case "--ignore=FOLDER" is passed as an argument
-    # Which might be the case if we are ignoring some providers during 
compatibility checks
-    pytest_args_before_skip = pytest_args
-    pytest_args = [arg for arg in pytest_args if f"--ignore={arg}" not in 
pytest_args]
-    # If no test directory is left (all positional args were 
excluded/ignored), skip
-    if pytest_args_before_skip != pytest_args and 
pytest_args[0].startswith("--"):
+    # Drop the test paths the test type selects under an "--ignore=FOLDER" 
argument, which is the case when
+    # some providers are skipped during compatibility checks: pytest runs an 
explicitly selected path even
+    # when it or an ancestor is ignored. Paths the user passes explicitly go 
to pytest as they are.
+    ignored_folders = [arg.removeprefix("--ignore=") for arg in pytest_args if 
arg.startswith("--ignore=")]
+    selected_paths = get_selected_test_paths(
+        test_group=shell_params.test_group,
+        test_type=shell_params.test_type,
+        skip_db_tests=shell_params.skip_db_tests,
+        parallel_test_types_list=shell_params.parallel_test_types_list,
+        integration=shell_params.integration,
+    )
+    ignored_selections = {path for path in selected_paths if 
is_path_under_any(path, ignored_folders)}
+    if selected_paths and ignored_selections == set(selected_paths):
         return 0, f"Skipped test, no tests needed: {shell_params.test_type}"
+    pytest_args = [arg for arg in pytest_args if arg not in ignored_selections]
     run_cmd.extend(pytest_args)
     try:
         remove_docker_networks(networks=[f"{compose_project_name}_default"])
@@ -391,6 +403,11 @@ def _run_tests_in_pool(
     # tests are still running. We are only adding here test types that take 
more than 2 minutes to run
     # on a fast machine in parallel
     sorting_order = [
+        *(
+            test_type
+            for provider_id in PROVIDER_TEST_SPLITS
+            for test_type in get_provider_split_test_types(provider_id)
+        ),
         "Providers[standard]",
         "Providers[amazon]",
         "Providers[google]",
diff --git a/dev/breeze/src/airflow_breeze/global_constants.py 
b/dev/breeze/src/airflow_breeze/global_constants.py
index 05b923f7669..e2b94d3d528 100644
--- a/dev/breeze/src/airflow_breeze/global_constants.py
+++ b/dev/breeze/src/airflow_breeze/global_constants.py
@@ -974,6 +974,42 @@ NUMBER_OF_LOW_DEP_SLICES = 5
 # Number of slices for core test types (splitting reduces memory per xdist 
collection)
 NUMBER_OF_CORE_SLICES = 2
 
+# Slice counts on the self-hosted RunsOn runners. Jobs using 
`--run-in-parallel` run one serial pytest per
+# test type, so a job never uses more CPUs than it has test types. The core 
jobs are bound by their slowest
+# test type and the Non-DB xdist collection gets enough memory on RunsOn, so 
one slice only removes jobs.
+# Low dep providers run on 32-CPU runners and the heaviest test types start 
first, so about 40 test types per
+# job keep those CPUs busy while the short ones fill in around the long ones.
+NUMBER_OF_CORE_SLICES_ON_RUNS_ON = 1
+LOW_DEP_TEST_TYPES_PER_JOB_ON_RUNS_ON = 40
+
+# On RunsOn the longest-running providers are split further in the 
individually listed provider test types,
+# so every part runs in its own pytest process: `Providers[<id>:<part>]` runs 
the listed paths (relative to the
+# provider's `tests` folder), and `Providers[<id>:other]` runs the rest of the 
provider's tests.
+PROVIDER_TEST_SPLITS: dict[str, dict[str, list[str]]] = {
+    "amazon": {
+        "hooks": ["unit/amazon/aws/hooks"],
+        "operators": ["unit/amazon/aws/operators"],
+    },
+    "google": {
+        "cloud-hooks": ["unit/google/cloud/hooks"],
+        "cloud-operators": ["unit/google/cloud/operators"],
+    },
+    "standard": {
+        "decorators": ["unit/standard/decorators"],
+        "python": ["unit/standard/operators/test_python.py"],
+    },
+}
+PROVIDER_TEST_SPLIT_OTHER_PART = "other"
+
+
+def get_provider_split_test_types(provider_id: str) -> list[str]:
+    """Test types running the tests of a split provider, or the provider's 
single test type."""
+    parts = PROVIDER_TEST_SPLITS.get(provider_id)
+    if not parts:
+        return [f"Providers[{provider_id}]"]
+    return [f"Providers[{provider_id}:{part}]" for part in [*parts, 
PROVIDER_TEST_SPLIT_OTHER_PART]]
+
+
 # Milestone Tag Assistant configuration
 # Labels indicating a bug fix PR that should have a milestone
 MILESTONE_BUG_LABELS: frozenset[str] = frozenset({"kind:bug", "type:bug-fix"})
diff --git a/dev/breeze/src/airflow_breeze/params/shell_params.py 
b/dev/breeze/src/airflow_breeze/params/shell_params.py
index 8c779099f7d..ce10da06219 100644
--- a/dev/breeze/src/airflow_breeze/params/shell_params.py
+++ b/dev/breeze/src/airflow_breeze/params/shell_params.py
@@ -265,6 +265,8 @@ class ShellParams:
     def clone_with_test(self, test_type: str) -> ShellParams:
         new_params = deepcopy(self)
         new_params.test_type = test_type
+        # The clone runs this one test type: kept, the list would make it 
select every test type's folders.
+        new_params.parallel_test_types_list = []
         return new_params
 
     @cached_property
diff --git a/dev/breeze/src/airflow_breeze/utils/ci_minutes_estimate.py 
b/dev/breeze/src/airflow_breeze/utils/ci_minutes_estimate.py
index fd51a2140b5..5a3470091f8 100644
--- a/dev/breeze/src/airflow_breeze/utils/ci_minutes_estimate.py
+++ b/dev/breeze/src/airflow_breeze/utils/ci_minutes_estimate.py
@@ -125,11 +125,13 @@ def count_provider_test_files(provider_id: str) -> int:
 
 def find_tested_provider_ids(provider_test_types: list[str]) -> list[str] | 
None:
     """Return the ids of individually tested providers, or None when all 
providers are tested."""
-    provider_ids = []
+    provider_ids: list[str] = []
     for test_type in provider_test_types:
         if not test_type.startswith("Providers[") or 
test_type.startswith("Providers[-"):
             return None
-        provider_ids.extend(test_type[len("Providers[") : -1].split(","))
+        provider_ids.extend(
+            provider_id.partition(":")[0] for provider_id in 
test_type[len("Providers[") : -1].split(",")
+        )
     return provider_ids
 
 
diff --git a/dev/breeze/src/airflow_breeze/utils/run_tests.py 
b/dev/breeze/src/airflow_breeze/utils/run_tests.py
index ef87dd1f6e3..5391df135be 100644
--- a/dev/breeze/src/airflow_breeze/utils/run_tests.py
+++ b/dev/breeze/src/airflow_breeze/utils/run_tests.py
@@ -26,6 +26,8 @@ from airflow_breeze.global_constants import (
     ALL_TEST_SUITES,
     ALL_TEST_TYPE,
     NONE_TEST_TYPE,
+    PROVIDER_TEST_SPLIT_OTHER_PART,
+    PROVIDER_TEST_SPLITS,
     GroupOfTests,
     SelectiveCoreTestType,
     all_helm_test_packages,
@@ -201,7 +203,8 @@ def run_docker_compose_tests(
 
 def file_name_from_test_type(test_type: str):
     test_type_no_brackets = test_type.lower().replace("[", "_").replace("]", 
"")
-    return re.sub("[,.]", "_", test_type_no_brackets)[:30]
+    # Also names the docker compose project, which allows only lowercase 
letters, digits, "-" and "_".
+    return re.sub("[^a-z0-9_-]", "_", test_type_no_brackets)[:30]
 
 
 def test_paths(test_type: str, backend: str) -> tuple[str, str, str]:
@@ -241,23 +244,17 @@ def get_excluded_test_provider_folders(python_version: 
str) -> list[str]:
     return get_test_folders(excluded_folders)
 
 
-def are_all_test_paths_excluded(
+def get_selected_test_paths(
     *,
     test_group: GroupOfTests,
     test_type: str,
-    python_version: str,
     skip_db_tests: bool = False,
     parallel_test_types_list: list[str] | None = None,
     integration: tuple | None = None,
-) -> bool:
-    """Check if all test paths for the given test type are excluded or 
suspended.
-
-    Uses the same provider.yaml exclusion data as generate_args_for_pytest to 
determine
-    whether every test directory that would be generated for this test type is 
in the
-    excluded or suspended provider set.
+) -> list[str]:
+    """The test paths generate_args_for_pytest selects for the test type, 
without the pytest options.
 
-    Returns False when test_type is "None" (user provides explicit paths via 
extra_pytest_args)
-    or when any test path remains after exclusions.
+    Empty when test_type is "None": the user then provides explicit paths via 
extra_pytest_args.
     """
     if skip_db_tests and parallel_test_types_list:
         initial_paths = convert_parallel_types_to_folders(
@@ -270,13 +267,51 @@ def are_all_test_paths_excluded(
             test_type=test_type,
             integration=integration,
         )
-    # Filter to only actual test directory paths (not pytest flags like -m, 
--include-quarantined)
-    test_dir_paths = [p for p in initial_paths if not p.startswith("-")]
+    # Only the test folders: test suites such as "All-Postgres" add pytest 
options with values too
+    return [p for p in initial_paths if is_path_under_any(p, 
get_all_test_group_folders())]
+
+
+def get_all_test_group_folders() -> list[str]:
+    return [folder for group_folders in TEST_GROUP_TO_TEST_FOLDERS.values() 
for folder in group_folders]
+
+
+def is_path_under_any(path: str, folders: list[str]) -> bool:
+    path = path.rstrip("/")
+    return any(path == folder.rstrip("/") or 
path.startswith(f"{folder.rstrip('/')}/") for folder in folders)
+
+
+def are_all_test_paths_excluded(
+    *,
+    test_group: GroupOfTests,
+    test_type: str,
+    python_version: str,
+    skip_db_tests: bool = False,
+    parallel_test_types_list: list[str] | None = None,
+    integration: tuple | None = None,
+) -> bool:
+    """Check if all test paths for the given test type are excluded or 
suspended.
+
+    Uses the same provider.yaml exclusion data as generate_args_for_pytest to 
determine
+    whether every test directory that would be generated for this test type is 
in the
+    excluded or suspended provider set.
+
+    Returns False when test_type is "None" (user provides explicit paths via 
extra_pytest_args)
+    or when any test path remains after exclusions.
+    """
+    test_dir_paths = get_selected_test_paths(
+        test_group=test_group,
+        test_type=test_type,
+        skip_db_tests=skip_db_tests,
+        parallel_test_types_list=parallel_test_types_list,
+        integration=integration,
+    )
     if not test_dir_paths:
         return False
-    excluded = set(get_excluded_test_provider_folders(python_version))
-    suspended = set(get_suspended_test_provider_folders())
-    return all(p in excluded | suspended for p in test_dir_paths)
+    excluded_or_suspended = [
+        *get_excluded_test_provider_folders(python_version),
+        *get_suspended_test_provider_folders(),
+    ]
+    return all(is_path_under_any(p, excluded_or_suspended) for p in 
test_dir_paths)
 
 
 TEST_TYPE_CORE_MAP_TO_PYTEST_ARGS: dict[str, list[str]] = {
@@ -382,7 +417,10 @@ def is_provider_selected_in_test_types(provider_id: str, 
test_types: list[str])
             if provider_id not in excluded:
                 return True
         elif test_type.startswith(PROVIDERS_LIST_PREFIX):
-            included = test_type[len(PROVIDERS_LIST_PREFIX) : -1].split(",")
+            included = [
+                included_id.partition(":")[0]
+                for included_id in test_type[len(PROVIDERS_LIST_PREFIX) : 
-1].split(",")
+            ]
             if provider_id in included:
                 return True
         else:
@@ -447,6 +485,8 @@ def convert_test_type_to_pytest_args(
                     )
                     providers_with_exclusions.remove(provider_test_to_exclude)
             return providers_with_exclusions
+        if test_type.startswith(PROVIDERS_LIST_PREFIX) and ":" in test_type:
+            return 
convert_provider_part_to_pytest_args(test_type[len(PROVIDERS_LIST_PREFIX) : -1])
         if test_type.startswith(PROVIDERS_LIST_PREFIX):
             provider_list = test_type[len(PROVIDERS_LIST_PREFIX) : 
-1].split(",")
             providers_to_test = []
@@ -482,6 +522,22 @@ def convert_test_type_to_pytest_args(
     sys.exit(1)
 
 
+def convert_provider_part_to_pytest_args(provider_part: str) -> list[str]:
+    """Pytest args for one part of a split provider, given as 
``<provider_id>:<part>``."""
+    provider_id, _, part = provider_part.partition(":")
+    parts = PROVIDER_TEST_SPLITS.get(provider_id, {})
+    tests_folder = f"providers/{provider_id.replace('.', '/')}/tests"
+    if part == PROVIDER_TEST_SPLIT_OTHER_PART and parts:
+        return [
+            tests_folder,
+            *(f"--ignore={tests_folder}/{path}" for paths in parts.values() 
for path in paths),
+        ]
+    if part not in parts:
+        console_print(f"[error]Unknown part {part!r} of the {provider_id!r} 
provider tests[/]")
+        sys.exit(1)
+    return [f"{tests_folder}/{path}" for path in parts[part]]
+
+
 def generate_args_for_pytest(
     *,
     test_group: GroupOfTests,
@@ -618,6 +674,12 @@ def generate_args_for_pytest(
 
 
 def convert_parallel_types_to_folders(test_group: GroupOfTests, 
parallel_test_types_list: list[str]):
+    # All test types share one pytest here, where the --ignore of a 
"<provider>:other" part would also drop
+    # the provider's other parts, so a split provider runs as a whole: 
Providers[<provider>].
+    split_parts = [test_type for test_type in parallel_test_types_list if ":" 
in test_type]
+    if split_parts:
+        console_print(f"[error]Split provider test types cannot run in one 
pytest process: {split_parts}[/]")
+        sys.exit(1)
     args = []
     for _test_type in parallel_test_types_list:
         args.extend(
@@ -626,9 +688,6 @@ def convert_parallel_types_to_folders(test_group: 
GroupOfTests, parallel_test_ty
                 test_type=_test_type,
             )
         )
-    all_test_prefixes: list[str] = []
+    all_test_prefixes = get_all_test_group_folders()
     # leave only folders, strip --pytest-args that exclude some folders with 
`-` prefix
-    for group_folders in TEST_GROUP_TO_TEST_FOLDERS.values():
-        for group_folder in group_folders:
-            all_test_prefixes.append(group_folder)
     return [arg for arg in args if any(arg.startswith(prefix) for prefix in 
all_test_prefixes)]
diff --git a/dev/breeze/src/airflow_breeze/utils/selective_checks.py 
b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
index bc2922d3bc3..7bde4b0237b 100644
--- a/dev/breeze/src/airflow_breeze/utils/selective_checks.py
+++ b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
@@ -20,6 +20,7 @@ import ast
 import difflib
 import itertools
 import json
+import math
 import os
 import re
 import sys
@@ -34,6 +35,7 @@ from typing import Any, TypeVar
 from airflow_breeze.branch_defaults import AIRFLOW_BRANCH, 
DEFAULT_AIRFLOW_CONSTRAINTS_BRANCH
 from airflow_breeze.global_constants import (
     ALL_PYTHON_MAJOR_MINOR_VERSIONS,
+    ALL_TEST_TYPE,
     APACHE_AIRFLOW_GITHUB_REPOSITORY,
     CI_AMD_PLATFORM,
     CI_ARM_PLATFORM,
@@ -51,8 +53,11 @@ from airflow_breeze.global_constants import (
     HELM_VERSION,
     JAVA_SDK_VERSION,
     KIND_VERSION,
+    LOW_DEP_TEST_TYPES_PER_JOB_ON_RUNS_ON,
     NUMBER_OF_CORE_SLICES,
+    NUMBER_OF_CORE_SLICES_ON_RUNS_ON,
     NUMBER_OF_LOW_DEP_SLICES,
+    PROVIDER_TEST_SPLITS,
     PROVIDERS_COMPATIBILITY_TESTS_MATRIX,
     PUBLIC_AMD_RUNNERS,
     PUBLIC_ARM_RUNNERS,
@@ -72,6 +77,7 @@ from airflow_breeze.global_constants import (
     all_helm_test_packages,
     all_selective_core_test_types,
     get_airflow_version,
+    get_provider_split_test_types,
     providers_test_type,
 )
 from airflow_breeze.utils.console import console_print, get_console
@@ -905,11 +911,13 @@ def _get_test_type_description(provider_test_types: 
list[str]) -> str:
         first_provider = first_provider.replace("Providers[", "").replace("]", 
"")
     if last_provider.startswith("Providers["):
         last_provider = last_provider.replace("Providers[", "").replace("]", 
"")
-    return (
+    description = (
         f"{first_provider[:13]}...{last_provider[:13]}"
         if first_provider != last_provider
         else (first_provider[:29])
     )
+    # The description ends up in artifact names (via JOB_ID), which must not 
contain ":".
+    return description.replace(":", "-")
 
 
 def _get_test_list_as_json(list_of_list_of_types: list[list[str]]) -> 
list[dict[str, str]] | None:
@@ -1911,9 +1919,10 @@ class SelectiveChecks:
         if not self.run_unit_tests:
             return None
         current_test_types = sorted(set(self._get_core_test_types_to_run()))
-        if len(current_test_types) <= NUMBER_OF_CORE_SLICES:
+        number_of_slices = NUMBER_OF_CORE_SLICES_ON_RUNS_ON if 
self._is_on_runs_on else NUMBER_OF_CORE_SLICES
+        if len(current_test_types) <= number_of_slices:
             return json.dumps(_get_test_list_as_json([current_test_types]))
-        list_of_list_of_types = _split_list(current_test_types, 
NUMBER_OF_CORE_SLICES)
+        list_of_list_of_types = _split_list(current_test_types, 
number_of_slices)
         return json.dumps(_get_test_list_as_json(list_of_list_of_types))
 
     @cached_property
@@ -1963,10 +1972,7 @@ class SelectiveChecks:
             if kept:
                 current_test_types.add(f"Providers[{','.join(kept)}]")
 
-    @cached_property
-    def providers_test_types_list_as_strings_in_json(self) -> str:
-        if not self.run_unit_tests:
-            return "[]"
+    def _get_providers_test_types_for_branch(self) -> set[str]:
         current_test_types = set(self._get_providers_test_types_to_run())
         if self._default_branch != "main":
             test_types_to_remove: set[str] = set()
@@ -1978,10 +1984,44 @@ class SelectiveChecks:
                     )
                     test_types_to_remove.add(test_type)
             current_test_types = current_test_types - test_types_to_remove
+        return current_test_types
+
+    @cached_property
+    def providers_test_types_list_as_strings_in_json(self) -> str:
+        if not self.run_unit_tests:
+            return "[]"
+        current_test_types = self._get_providers_test_types_for_branch()
         self._extract_long_provider_tests(current_test_types)
         self._filter_platform_excluded_test_types(current_test_types)
         return json.dumps(_get_test_list_as_json([sorted(current_test_types)]))
 
+    @cached_property
+    def providers_split_test_types_list_as_strings_in_json(self) -> str:
+        """Provider test types for the jobs running them in parallel in the CI 
image.
+
+        On RunsOn every provider (and every part of a split provider) is a 
test type of its own, in one
+        job: one serial pytest per provider keeps the 32 CPUs of the runner 
busy, where grouped providers
+        would leave the job waiting on the large "all other providers" group. 
The compatibility tests keep
+        the grouped list, as each of their test types installs an Airflow 
version first.
+        """
+        if not self.run_unit_tests:
+            return "[]"
+        if self._is_on_runs_on and self._get_providers_test_types_for_branch():
+            return 
json.dumps(_get_test_list_as_json([sorted(self._get_individual_providers_list())]))
+        return self.providers_test_types_list_as_strings_in_json
+
+    @cached_property
+    def providers_xdist_test_types_list_as_strings_in_json(self) -> str:
+        """Provider test types for the xdist jobs, which run them all in one 
pytest, so none is split out."""
+        if not self.run_unit_tests:
+            return "[]"
+        current_test_types = self._get_providers_test_types_for_branch()
+        if "Providers" in current_test_types and 
self._platform_excluded_providers:
+            current_test_types.remove("Providers")
+            
current_test_types.add(f"Providers[-{','.join(sorted(self._platform_excluded_providers))}]")
+        self._filter_platform_excluded_test_types(current_test_types)
+        return json.dumps(_get_test_list_as_json([sorted(current_test_types)]))
+
     def _get_individual_providers_list(self):
         current_test_types = 
set(self._get_providers_test_types_to_run(split_to_individual_providers=True))
         if "Providers" in current_test_types:
@@ -1990,6 +2030,12 @@ class SelectiveChecks:
                 {f"Providers[{provider}]" for provider in 
get_available_distributions(include_not_ready=True)}
             )
         self._filter_platform_excluded_test_types(current_test_types)
+        if not self._is_on_runs_on:
+            return current_test_types
+        for provider_id in PROVIDER_TEST_SPLITS:
+            if f"Providers[{provider_id}]" in current_test_types:
+                current_test_types.remove(f"Providers[{provider_id}]")
+                
current_test_types.update(get_provider_split_test_types(provider_id))
         return current_test_types
 
     @cached_property
@@ -2000,11 +2046,14 @@ class SelectiveChecks:
         current_test_types = sorted(self._get_individual_providers_list())
         if not current_test_types:
             return None
-        # We are hard-coding the number of lists as reasonable starting point 
to split the
-        # list of test types - and we can modify it in the future
-        if len(current_test_types) < NUMBER_OF_LOW_DEP_SLICES:
+        number_of_slices = (
+            math.ceil(len(current_test_types) / 
LOW_DEP_TEST_TYPES_PER_JOB_ON_RUNS_ON)
+            if self._is_on_runs_on
+            else NUMBER_OF_LOW_DEP_SLICES
+        )
+        if len(current_test_types) < number_of_slices:
             return json.dumps(_get_test_list_as_json([current_test_types]))
-        list_of_list_of_types = _split_list(current_test_types, 
NUMBER_OF_LOW_DEP_SLICES)
+        list_of_list_of_types = _split_list(current_test_types, 
number_of_slices)
         return json.dumps(_get_test_list_as_json(list_of_list_of_types))
 
     @cached_property
@@ -2284,6 +2333,10 @@ class SelectiveChecks:
 
     @cached_property
     def helm_test_packages(self) -> str:
+        # A Helm test package takes about a minute under xdist on a RunsOn 
runner, against two to three
+        # minutes of preparing the image, so RunsOn runs all of them in one 
job.
+        if self._is_on_runs_on:
+            return json.dumps([ALL_TEST_TYPE])
         return json.dumps(all_helm_test_packages())
 
     @cached_property
@@ -2358,17 +2411,18 @@ class SelectiveChecks:
         )
         return json.dumps([template.format(run_id=run_id)])
 
+    @cached_property
+    def _is_on_runs_on(self) -> bool:
+        # Reuses the routing decision of runner_type, which may have asked the 
GitHub API.
+        return self.runner_type not in (PUBLIC_AMD_RUNNERS, PUBLIC_ARM_RUNNERS)
+
     @cached_property
     def in_memory_runner_type(self) -> str:
         """Runner labels like ``runner_type``, with docker on a RAM disk when 
the run is on RunsOn.
 
         The repository variable ``USE_IN_MEMORY_DOCKER=false`` keeps docker on 
disk.
         """
-        # Reuses the routing decision of runner_type, which may have asked the 
GitHub API.
-        if (
-            self.runner_type in (PUBLIC_AMD_RUNNERS, PUBLIC_ARM_RUNNERS)
-            or os.environ.get("USE_IN_MEMORY_DOCKER", "").casefold() == "false"
-        ):
+        if not self._is_on_runs_on or os.environ.get("USE_IN_MEMORY_DOCKER", 
"").casefold() == "false":
             return self.runner_type
         template = (
             RUNS_ON_ARM_IN_MEMORY_RUNNER_TEMPLATE
diff --git a/dev/breeze/tests/test_pytest_args_for_test_types.py 
b/dev/breeze/tests/test_pytest_args_for_test_types.py
index 4332d8dceb7..88444958690 100644
--- a/dev/breeze/tests/test_pytest_args_for_test_types.py
+++ b/dev/breeze/tests/test_pytest_args_for_test_types.py
@@ -20,7 +20,11 @@ import pytest
 
 from airflow_breeze.global_constants import GroupOfTests
 from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH
-from airflow_breeze.utils.run_tests import convert_parallel_types_to_folders, 
convert_test_type_to_pytest_args
+from airflow_breeze.utils.run_tests import (
+    convert_parallel_types_to_folders,
+    convert_test_type_to_pytest_args,
+    file_name_from_test_type,
+)
 
 
 def _all_providers() -> list[str]:
@@ -214,6 +218,53 @@ def test_pytest_args_for_missing_provider():
         )
 
 
[email protected](
+    ("test_type", "pytest_args"),
+    [
+        pytest.param(
+            "Providers[amazon:hooks]",
+            ["providers/amazon/tests/unit/amazon/aws/hooks"],
+            id="named part runs its paths",
+        ),
+        pytest.param(
+            "Providers[standard:python]",
+            
["providers/standard/tests/unit/standard/operators/test_python.py"],
+            id="a part can be a single file",
+        ),
+        pytest.param(
+            "Providers[amazon:other]",
+            [
+                "providers/amazon/tests",
+                "--ignore=providers/amazon/tests/unit/amazon/aws/hooks",
+                "--ignore=providers/amazon/tests/unit/amazon/aws/operators",
+            ],
+            id="other part ignores the named parts",
+        ),
+    ],
+)
+def test_pytest_args_for_split_provider_parts(test_type: str, pytest_args: 
list[str]):
+    assert (
+        convert_test_type_to_pytest_args(test_group=GroupOfTests.PROVIDERS, 
test_type=test_type)
+        == pytest_args
+    )
+
+
[email protected](
+    "test_type", ["Providers[amazon:missing]", "Providers[airbyte:other]"], 
ids=["unknown part", "unsplit"]
+)
+def test_pytest_args_for_unknown_provider_part(test_type: str):
+    with pytest.raises(SystemExit):
+        convert_test_type_to_pytest_args(test_group=GroupOfTests.PROVIDERS, 
test_type=test_type)
+
+
+def test_folders_for_parallel_test_types_reject_split_provider_parts():
+    with pytest.raises(SystemExit):
+        convert_parallel_types_to_folders(
+            test_group=GroupOfTests.PROVIDERS,
+            parallel_test_types_list=["Providers[amazon:hooks]", 
"Providers[amazon:other]"],
+        )
+
+
 @pytest.mark.parametrize(
     ("test_group", "parallel_test_types", "folders"),
     [
@@ -394,3 +445,15 @@ def xtest_wrong_types_for_parallel_test_types(test_group: 
GroupOfTests, parallel
             test_group=test_group,
             parallel_test_types_list=parallel_test_types.split(" "),
         )
+
+
[email protected](
+    ("test_type", "file_name"),
+    [
+        pytest.param("Providers[amazon:hooks]", "providers_amazon_hooks", 
id="split provider part"),
+        pytest.param("Providers[google:cloud-hooks]", 
"providers_google_cloud-hooks", id="part with a dash"),
+        pytest.param("Providers[apache.hive,http]", 
"providers_apache_hive_http", id="provider list"),
+    ],
+)
+def test_file_name_from_test_type_is_a_valid_compose_project_name(test_type: 
str, file_name: str):
+    assert file_name_from_test_type(test_type) == file_name
diff --git a/dev/breeze/tests/test_run_test_args.py 
b/dev/breeze/tests/test_run_test_args.py
index b146e61d305..bbcbb598e21 100644
--- a/dev/breeze/tests/test_run_test_args.py
+++ b/dev/breeze/tests/test_run_test_args.py
@@ -23,6 +23,7 @@ import pytest
 from airflow_breeze.commands.testing_commands import _run_test, 
pull_testcontainers_images
 from airflow_breeze.global_constants import TESTCONTAINERS_IMAGES_BY_PROVIDER, 
GroupOfTests
 from airflow_breeze.params.shell_params import ShellParams
+from airflow_breeze.utils.run_tests import is_provider_selected_in_test_types
 
 
 @pytest.fixture(autouse=True)
@@ -109,6 +110,70 @@ def 
test_test_is_skipped_if_all_are_ignored(mock_run_command):
     mock_run_command.assert_called_once()  # called only to compose down
 
 
+def 
test_split_provider_part_is_skipped_if_its_provider_is_ignored(mock_run_command):
+    _run_test(
+        shell_params=ShellParams(test_group=GroupOfTests.PROVIDERS, 
test_type="Providers[amazon:hooks]"),
+        extra_pytest_args=("--ignore=providers/amazon/tests",),
+        python_version="3.9",
+        output=None,
+        test_timeout=60,
+        skip_docker_compose_down=True,
+    )
+
+    mock_run_command.assert_called_once()  # called only to compose down
+
+
+def 
test_test_is_skipped_if_its_only_path_is_ignored_next_to_path_valued_options(mock_run_command):
+    _run_test(
+        shell_params=ShellParams(test_group=GroupOfTests.PROVIDERS, 
test_type="Providers[amazon:hooks]"),
+        extra_pytest_args=(
+            "--ignore=providers/amazon/tests",
+            "--basetemp",
+            "/tmp/breeze-pytest",
+            "--rootdir",
+            "airflow-core/tests",
+        ),
+        python_version="3.9",
+        output=None,
+        test_timeout=60,
+        skip_docker_compose_down=True,
+    )
+
+    mock_run_command.assert_called_once()  # called only to compose down
+
+
+def test_test_suite_is_skipped_if_all_its_paths_are_ignored(mock_run_command):
+    _run_test(
+        shell_params=ShellParams(test_group=GroupOfTests.CORE, 
test_type="All-Postgres"),
+        extra_pytest_args=("--ignore=airflow-core/tests/unit",),
+        python_version="3.9",
+        output=None,
+        test_timeout=60,
+        skip_docker_compose_down=True,
+    )
+
+    mock_run_command.assert_called_once()  # called only to compose down
+
+
+def test_explicit_paths_go_to_pytest_as_they_are(mock_run_command):
+    _run_test(
+        shell_params=ShellParams(test_group=GroupOfTests.PROVIDERS, 
test_type="None"),
+        extra_pytest_args=(
+            "providers/amazon/tests/unit/amazon/aws/hooks",
+            "providers/http/tests",
+            "--ignore=providers/amazon/tests",
+        ),
+        python_version="3.9",
+        output=None,
+        test_timeout=60,
+        skip_docker_compose_down=True,
+    )
+
+    run_cmd = mock_run_command.call_args_list[1].args[0]
+    assert "providers/http/tests" in run_cmd
+    assert "providers/amazon/tests/unit/amazon/aws/hooks" in run_cmd
+
+
 def test_test_is_skipped_when_all_providers_excluded_for_python_version(
     mock_run_command, mock_get_excluded_provider_folders
 ):
@@ -127,6 +192,34 @@ def 
test_test_is_skipped_when_all_providers_excluded_for_python_version(
     mock_run_command.assert_not_called()
 
 
+def 
test_split_provider_part_is_skipped_when_provider_excluded_for_python_version(
+    mock_run_command, mock_get_excluded_provider_folders
+):
+    mock_get_excluded_provider_folders.return_value = ["amazon"]
+    return_code, message = _run_test(
+        shell_params=ShellParams(test_group=GroupOfTests.PROVIDERS, 
test_type="Providers[amazon:hooks]"),
+        extra_pytest_args=(),
+        python_version="3.14",
+        output=None,
+        test_timeout=60,
+        skip_docker_compose_down=True,
+    )
+    assert return_code == 0
+    assert "Skipped" in message
+    mock_run_command.assert_not_called()
+
+
[email protected](
+    ("test_types", "expected"),
+    [
+        pytest.param(["Providers[amazon:hooks]"], True, id="part of the 
provider"),
+        pytest.param(["Providers[google:cloud-hooks]"], False, id="part of 
another provider"),
+    ],
+)
+def test_split_provider_part_selects_its_provider(test_types: list[str], 
expected: bool):
+    assert is_provider_selected_in_test_types("amazon", test_types) is expected
+
+
 def test_test_is_not_skipped_when_some_providers_remain(mock_run_command, 
mock_get_excluded_provider_folders):
     """When only some providers are excluded, the test should still run."""
     mock_get_excluded_provider_folders.return_value = ["http"]
diff --git a/dev/breeze/tests/test_selective_checks.py 
b/dev/breeze/tests/test_selective_checks.py
index 5cdac3e1e94..bf4e2370a39 100644
--- a/dev/breeze/tests/test_selective_checks.py
+++ b/dev/breeze/tests/test_selective_checks.py
@@ -37,10 +37,13 @@ from airflow_breeze.global_constants import (
     DEFAULT_PYTHON_MAJOR_MINOR_VERSION,
     JAVA_SDK_VERSION,
     NUMBER_OF_CORE_SLICES,
+    NUMBER_OF_CORE_SLICES_ON_RUNS_ON,
     NUMBER_OF_LOW_DEP_SLICES,
     PROVIDERS_COMPATIBILITY_TESTS_MATRIX,
     GithubEvents,
+    all_helm_test_packages,
     get_airflow_version,
+    get_provider_split_test_types,
 )
 from airflow_breeze.utils.functools_cache import clearable_cache
 from airflow_breeze.utils.packages import get_available_distributions
@@ -111,6 +114,12 @@ LIST_OF_ALL_PROVIDER_TESTS = [
     f"Providers[{provider}]" for provider in 
get_available_distributions(include_not_ready=True)
 ]
 
+LIST_OF_ALL_PROVIDER_TESTS_ON_RUNS_ON = [
+    test_type
+    for provider in get_available_distributions(include_not_ready=True)
+    for test_type in get_provider_split_test_types(provider)
+]
+
 LIST_OF_ALL_PROVIDER_TESTS_AS_JSON = json.dumps(
     _get_test_list_as_json(_split_list(sorted(LIST_OF_ALL_PROVIDER_TESTS), 5))
 )
@@ -2328,6 +2337,165 @@ def test_list_splitting():
     )
 
 
[email protected](
+    (
+        "github_event",
+        "github_context_dict",
+        "expected_core_slices",
+        "expected_low_dep_slices",
+        "expected_low_dep_test_types",
+    ),
+    [
+        pytest.param(
+            GithubEvents.PULL_REQUEST,
+            {},
+            NUMBER_OF_CORE_SLICES,
+            NUMBER_OF_LOW_DEP_SLICES,
+            LIST_OF_ALL_PROVIDER_TESTS,
+            id="public runners",
+        ),
+        pytest.param(
+            GithubEvents.PUSH,
+            {"run_id": 42},
+            NUMBER_OF_CORE_SLICES_ON_RUNS_ON,
+            3,
+            LIST_OF_ALL_PROVIDER_TESTS_ON_RUNS_ON,
+            id="RunsOn runners",
+        ),
+    ],
+)
+def test_number_of_test_type_slices_follows_runners(
+    github_event: GithubEvents,
+    github_context_dict: dict[str, Any],
+    expected_core_slices: int,
+    expected_low_dep_slices: int,
+    expected_low_dep_test_types: list[str],
+):
+    selective_checks = SelectiveChecks(
+        pr_labels=("full tests needed",),
+        default_branch="main",
+        github_event=github_event,
+        github_repository="apache/airflow",
+        github_context_dict=github_context_dict,
+    )
+    core_slices = 
json.loads(selective_checks.core_test_types_list_as_strings_in_json)
+    low_dep_slices = 
json.loads(selective_checks.individual_providers_test_types_list_as_strings_in_json)
+    assert len(core_slices) == expected_core_slices
+    assert len(low_dep_slices) == expected_low_dep_slices
+    assert sorted(" ".join(s["test_types"] for s in core_slices).split()) == 
sorted(
+        ALL_CI_SELECTIVE_TEST_TYPES.split()
+    )
+    assert sorted(" ".join(s["test_types"] for s in low_dep_slices).split()) 
== sorted(
+        expected_low_dep_test_types
+    )
+
+
[email protected](
+    ("github_event", "github_context_dict", "expected_test_types"),
+    [
+        pytest.param(
+            GithubEvents.PULL_REQUEST,
+            {},
+            [
+                "Providers[-amazon,celery,google,standard]",
+                "Providers[amazon]",
+                "Providers[celery]",
+                "Providers[google]",
+                "Providers[standard]",
+            ],
+            id="public runners group the providers",
+        ),
+        pytest.param(
+            GithubEvents.PUSH,
+            {"run_id": 42},
+            sorted(LIST_OF_ALL_PROVIDER_TESTS_ON_RUNS_ON),
+            id="RunsOn runners list each provider",
+        ),
+    ],
+)
+def test_providers_test_types_follow_runners(
+    github_event: GithubEvents, github_context_dict: dict[str, Any], 
expected_test_types: list[str]
+):
+    selective_checks = SelectiveChecks(
+        pr_labels=("full tests needed",),
+        default_branch="main",
+        github_event=github_event,
+        github_repository="apache/airflow",
+        github_context_dict=github_context_dict,
+    )
+    split_slices = 
json.loads(selective_checks.providers_split_test_types_list_as_strings_in_json)
+    assert [s["test_types"].split() for s in split_slices] == 
[expected_test_types]
+    # The compatibility tests install an Airflow version per test type, so 
they always get the groups
+    grouped_slices = 
json.loads(selective_checks.providers_test_types_list_as_strings_in_json)
+    assert [s["test_types"].split() for s in grouped_slices] == [
+        [
+            "Providers[-amazon,celery,google,standard]",
+            "Providers[amazon]",
+            "Providers[celery]",
+            "Providers[google]",
+            "Providers[standard]",
+        ]
+    ]
+
+
[email protected](
+    ("platform", "expected_test_types"),
+    [
+        pytest.param(CI_AMD_PLATFORM, "Providers", id="AMD"),
+        pytest.param(CI_ARM_PLATFORM, "Providers[-ibm.db2,ibm.mq]", id="ARM 
skips platform-excluded"),
+    ],
+)
+def test_providers_xdist_test_types_are_not_split(platform: str, 
expected_test_types: str):
+    selective_checks = SelectiveChecks(
+        files=("airflow-core/tests/test_example.py",),
+        commit_ref=NEUTRAL_COMMIT,
+        github_event=GithubEvents.PUSH,
+        github_repository="apache/airflow",
+        github_context_dict={"run_id": 42},
+        default_branch="main",
+        pr_labels=("full tests needed",),
+        platform=platform,
+    )
+    slices = 
json.loads(selective_checks.providers_xdist_test_types_list_as_strings_in_json)
+    assert [s["test_types"] for s in slices] == [expected_test_types]
+
+
[email protected](SelectiveChecks, "_is_on_runs_on", new=property(lambda self: 
True))
+def test_individual_providers_split_the_longest_running_providers_on_runs_on():
+    selective_checks = SelectiveChecks(
+        
files=("providers/amazon/src/airflow/providers/amazon/aws/hooks/s3.py",),
+        commit_ref=NEUTRAL_COMMIT,
+        default_branch="main",
+        github_event=GithubEvents.PULL_REQUEST,
+    )
+    slices = 
json.loads(selective_checks.individual_providers_test_types_list_as_strings_in_json)
+    assert len(slices) == 1
+    assert ":" not in slices[0]["description"]
+    test_types = slices[0]["test_types"].split()
+    assert "Providers[amazon]" not in test_types
+    assert {"Providers[amazon:hooks]", "Providers[amazon:operators]", 
"Providers[amazon:other]"} <= set(
+        test_types
+    )
+
+
[email protected](
+    ("github_event", "github_context_dict", "expected_packages"),
+    [
+        pytest.param(GithubEvents.PULL_REQUEST, {}, all_helm_test_packages(), 
id="public runners"),
+        pytest.param(GithubEvents.PUSH, {"run_id": 42}, ["All"], id="RunsOn 
runners"),
+    ],
+)
+def test_helm_test_packages_follow_runners(
+    github_event: GithubEvents, github_context_dict: dict[str, Any], 
expected_packages: list[str]
+):
+    selective_checks = SelectiveChecks(
+        github_event=github_event,
+        github_repository="apache/airflow",
+        github_context_dict=github_context_dict,
+    )
+    assert json.loads(selective_checks.helm_test_packages) == expected_packages
+
+
 def test_excluded_providers():
     stderr = SelectiveChecks(
         files=(),
diff --git a/dev/breeze/tests/test_shell_params.py 
b/dev/breeze/tests/test_shell_params.py
index 47655d7942a..60f12497712 100644
--- a/dev/breeze/tests/test_shell_params.py
+++ b/dev/breeze/tests/test_shell_params.py
@@ -29,7 +29,7 @@ from rich.console import Console
 
 from airflow_breeze.branch_defaults import AIRFLOW_BRANCH
 from airflow_breeze.commands.common_options import option_project_name
-from airflow_breeze.global_constants import MOUNT_SELECTED, 
PYCACHE_PREFIX_IN_CONTAINER
+from airflow_breeze.global_constants import MOUNT_SELECTED, 
PYCACHE_PREFIX_IN_CONTAINER, GroupOfTests
 from airflow_breeze.params.shell_params import ShellParams
 from airflow_breeze.utils.path_utils import (
     SCRIPTS_CI_DOCKER_COMPOSE_BASE_PATH,
@@ -431,3 +431,16 @@ def 
test_sphinx_airflow_theme_is_mounted_only_when_a_path_is_given(theme_path: s
     )
     # Compose interpolates the bind source from this variable, so it must be 
exported together with the file.
     assert 
shell_params.env_variables_for_docker_commands["SPHINX_AIRFLOW_THEME_PATH"] == 
theme_path
+
+
+def test_clone_with_test_runs_only_its_own_test_type():
+    shell_params = ShellParams(
+        test_group=GroupOfTests.PROVIDERS,
+        parallel_test_types_list=["Providers[amazon:hooks]", 
"Providers[amazon:other]"],
+    )
+
+    clone = shell_params.clone_with_test(test_type="Providers[amazon:hooks]")
+
+    assert clone.test_type == "Providers[amazon:hooks]"
+    assert clone.parallel_test_types_list == []
+    assert shell_params.parallel_test_types_list == 
["Providers[amazon:hooks]", "Providers[amazon:other]"]
diff --git a/scripts/ci/configure_runner_for_parallel_tests.sh 
b/scripts/ci/configure_runner_for_parallel_tests.sh
new file mode 100755
index 00000000000..4b97150972e
--- /dev/null
+++ b/scripts/ci/configure_runner_for_parallel_tests.sh
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Large runners run a test type per CPU, each a docker compose project with 
its own database container.
+#
+# Every test type of a parallel test run is a docker compose project with a 
network of its own. Docker's
+# default address pools hand out /16 and /20 subnets, which is about 30 
networks per host, so large runners
+# running a test type per CPU fail with "all predefined address pools have 
been fully subnetted".
+# Splitting the same 172.17.0.0/12 range into /24 subnets gives 4096 networks 
without claiming new addresses.
+set -euo pipefail
+
+DAEMON_JSON=/etc/docker/daemon.json
+
+sudo mkdir -p "$(dirname "${DAEMON_JSON}")"
+sudo python3 - "${DAEMON_JSON}" <<'EOF'
+import json
+import pathlib
+import sys
+
+daemon_json = pathlib.Path(sys.argv[1])
+config = json.loads(daemon_json.read_text()) if daemon_json.exists() and 
daemon_json.read_text().strip() else {}
+config["default-address-pools"] = [{"base": "172.17.0.0/12", "size": 24}]
+daemon_json.write_text(json.dumps(config, indent=2) + "\n")
+print(daemon_json.read_text())
+EOF
+sudo systemctl restart docker
+
+# Each MySQL server reserves Linux AIO contexts for InnoDB, and the default 
limit of 65536 runs out with a few
+# dozen of them: "io_setup() failed with EAGAIN".
+sudo sysctl -w fs.aio-max-nr=1048576
+docker info --format '{{.ServerVersion}}' > /dev/null
diff --git a/scripts/docker/entrypoint_ci.sh b/scripts/docker/entrypoint_ci.sh
index 1e61d0bd856..c487ea60a75 100755
--- a/scripts/docker/entrypoint_ci.sh
+++ b/scripts/docker/entrypoint_ci.sh
@@ -466,6 +466,8 @@ function check_force_lowest_dependencies() {
         local provider_id
         # shellcheck disable=SC2001
         provider_id=$(echo "${TEST_TYPE}" | sed 's/Providers\[\(.*\)\]/\1/')
+        # A split provider's test type names one part of its tests: 
Providers[<id>:<part>]
+        provider_id=${provider_id%%:*}
         echo
         echo "${COLOR_BLUE}Forcing dependencies to lowest versions for 
provider: ${provider_id}${COLOR_RESET}"
         echo

Reply via email to