Ir al contenido

Fragmentación de tests de Xcode

Requisitos
  • Análisis de tests de Xcode configurado. La fragmentación usa los datos de duración de los tests de los últimos 30 días para equilibrar los shards y, sin estos análisis, cada shard se ejecuta como si la suite fuera desconocida.

La fragmentación de tests para proyectos de Xcode usa tuist xcodebuild build-for-testing para crear un plan de shards y tuist xcodebuild test para ejecutar cada shard.

Cómo funciona#

La fragmentación de tests sigue un flujo de trabajo en dos fases:

  1. Fase de compilación: Tuist lee los módulos de test del archivo .xctestrun generado y crea un plan de shards en el servidor. El servidor usa datos históricos de duración de los tests de los últimos 30 días para distribuir los tests entre los shards, de modo que cada shard tarde aproximadamente lo mismo. La fase de compilación genera una matriz de shards que tu sistema de CI usa para lanzar runners en paralelo.
  2. Fase de test: Cada runner de integración continua recibe un índice de shard y el identificador exacto del plan de shards, y luego ejecuta la selección de tests de ese plan.

Con granularidad a nivel de suite, el servidor elige las suites conocidas por módulo: para cada módulo del .xctestrun actual, une las suites de las últimas ejecuciones de CI en la rama que se está compilando, recurriendo a la rama predeterminada del proyecto para los módulos sin historial en esa rama. Leer varias ejecuciones en lugar de solo la última es importante porque una ejecución fragmentada sube una ejecución de test por shard a medida que cada shard finaliza, por lo que la ejecución más reciente contiene solo la fracción del módulo que se ha procesado hasta ese momento, y porque evita que las ejecuciones de tests selectivos oculten suites que se omitieron en la última ejecución de la rama.

Cuando se usa --shard-granularity suite, Tuist equilibra las suites conocidas entre el número de shards solicitado y usa el último shard como comodín. Por ejemplo, --shard-total 5 produce índices de shard del 0 al 4, siendo el índice de shard 4 el comodín. Los shards normales se ejecutan con -only-testing para sus suites asignadas. El último shard se ejecuta sin -only-testing y pasa -skip-testing para cada suite asignada a los shards anteriores, de modo que ejecuta sus suites planificadas más las suites recién añadidas o las que faltan en el inventario histórico. La granularidad a nivel de módulo no necesita un comodín porque el archivo .xctestrun proporciona la lista de módulos.

Fase de compilación#

Compila tus tests y crea un plan de shards:

bash
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5

Este comando:

  1. Compila tus tests con xcodebuild build-for-testing
  2. Crea un plan de shards en el servidor de Tuist usando datos históricos de duración
  3. Sube el bundle .xctestproducts o escribe un archivo de shard para que lo usen los runners de shards
  4. Genera una matriz de shards para tu sistema de CI

Opciones de compilación#

FlagVariable de entornoDescripción
--shard-max <N>TUIST_TEST_SHARD_MAXNúmero máximo de shards. Se usa con --shard-max-duration para limitar la cantidad de shards
--shard-min <N>TUIST_TEST_SHARD_MINNúmero mínimo de shards
--shard-total <N>TUIST_TEST_SHARD_TOTALNúmero exacto de shards (excluyente con --shard-min/--shard-max). Con granularidad a nivel de suite, el último shard es el comodín
--shard-max-duration <MS>TUIST_TEST_SHARD_MAX_DURATIONDuración máxima objetivo por shard en milisegundos
--shard-granularity <LEVEL>TUIST_TEST_SHARD_GRANULARITYmodule (predeterminado) distribuye módulos de test completos entre los shards; suite distribuye clases de test individuales para un equilibrio más fino
--shard-reference <REF>TUIST_SHARD_REFERENCEIdentificador único para el plan de shards (se deriva automáticamente en proveedores de CI compatibles)
--shard-archive-path <PATH>TUIST_TEST_SHARD_ARCHIVE_PATHRuta donde Tuist escribe el archivo de shard optimizado en lugar de subir los productos de test al almacenamiento remoto

Fase de test#

Cada runner de shard ejecuta los tests que le han sido asignados:

bash
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'

Opciones de test#

FlagVariable de entornoDescripción
--shard-index <N>TUIST_SHARD_INDEXÍndice base cero del shard a ejecutar
--shard-plan-id <IDENTIFIER>TUIST_SHARD_PLAN_IDIdentificador exacto del plan de shards emitido por la fase de compilación. Las salidas generadas por el proveedor incluyen este valor automáticamente
--shard-reference <REF>TUIST_SHARD_REFERENCEIdentificador único para el plan de shards (se deriva automáticamente en proveedores de CI compatibles)
--shard-archive-path <PATH>TUIST_TEST_SHARD_ARCHIVE_PATHRuta a un archivo de shard gestionado localmente; Tuist lo extrae en lugar de descargar los productos de test desde el almacenamiento remoto

Tuist descarga el bundle .xctestproducts y lo filtra para incluir solo los tests asignados a ese shard.

Integración continua#

Tuist detecta automáticamente los siguientes proveedores de CI:

Para otros proveedores, consulta el archivo .tuist-shard-matrix.json para configurar trabajos paralelos.

GitHub Actions#

Usa una estrategia de matriz para ejecutar shards en paralelo:

yaml
name: Tests
on: [pull_request]
jobs:
build:
name: Build test shards
runs-on: macos-latest
outputs:
matrix: ${{ steps.build.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
- run: tuist auth login
- id: build
run: |
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5
test:
name: "Shard #${{ matrix.shard }}"
needs: build
runs-on: macos-latest
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.build.outputs.matrix) }}
env:
TUIST_SHARD_INDEX: ${{ matrix.shard }}
TUIST_SHARD_PLAN_ID: ${{ matrix.shard_plan_id }}
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
- run: tuist auth login
- run: |
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'

GitLab CI#

Tuist genera un .tuist-shard-child-pipeline.yml que activas como un pipeline hijo. Define un trabajo plantilla .tuist-shard que los trabajos de shard generados extienden:

yaml
# .gitlab-ci.yml
stages:
- build
- test
build-shards:
stage: build
tags: [macos]
script:
- tuist auth login
- |
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5
artifacts:
paths:
- .tuist-shard-child-pipeline.yml
test-shards:
stage: test
needs: [build-shards]
trigger:
include:
- artifact: .tuist-shard-child-pipeline.yml
job: build-shards
strategy: depend
yaml
# .gitlab/shard-template.yml
.tuist-shard:
tags: [macos]
script:
- tuist auth login
- |
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'

CircleCI#

Tuist genera un .tuist-shard-continuation.json con parámetros para el orb de continuación. El parámetro shard-plan-id debe declararse en tu configuración de continuación:

yaml
# .circleci/config.yml
version: 2.1
setup: true
orbs:
continuation: circleci/continuation@1
jobs:
build-shards:
macos:
xcode: "16.0"
steps:
- checkout
- run:
name: Build and plan shards
command: |
tuist auth login
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5
- continuation/continue:
configuration_path: .circleci/continue-config.yml
parameters: .tuist-shard-continuation.json
workflows:
setup:
jobs:
- build-shards
yaml
# .circleci/continue-config.yml
version: 2.1
parameters:
shard-indices:
type: string
default: ""
shard-count:
type: integer
default: 0
shard-plan-id:
type: string
default: ""
jobs:
test-shard:
macos:
xcode: "16.0"
parameters:
shard-index:
type: integer
steps:
- checkout
- run:
name: Run shard
command: |
export TUIST_SHARD_INDEX=<< parameters.shard-index >>
export TUIST_SHARD_PLAN_ID=<< pipeline.parameters.shard-plan-id >>
tuist auth login
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'
workflows:
test:
jobs:
- test-shard:
matrix:
parameters:
shard-index: [<< pipeline.parameters.shard-indices >>]

Buildkite#

Tuist genera un .tuist-shard-pipeline.yml con un paso por shard. Súbelo con buildkite-agent pipeline upload:

yaml
# pipeline.yml
steps:
- label: "Build test shards"
command: |
tuist auth login
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5
buildkite-agent pipeline upload .tuist-shard-pipeline.yml
agents:
queue: macos

Cada paso generado tiene TUIST_SHARD_INDEX y TUIST_SHARD_PLAN_ID configurados en su entorno. Añade el comando de test a cada paso de shard usando un script compartido:

bash
# .buildkite/shard-step.sh
#!/bin/bash
tuist auth login
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'

Codemagic#

Codemagic no admite trabajos de matriz dinámicos, así que define un flujo de trabajo separado por shard. Tuist escribe TUIST_SHARD_MATRIX y TUIST_SHARD_COUNT en el archivo CM_ENV para usarlos dentro del flujo de trabajo de compilación. El CM_ENV de Codemagic tiene alcance de flujo de trabajo, por lo que el identificador exacto del plan de shards no cruza los límites del flujo; los flujos de test recurren a la búsqueda de shards basada en referencia:

yaml
# codemagic.yaml
workflows:
build-shards:
name: Build test shards
instance_type: mac_mini_m2
environment:
xcode: latest
scripts:
- name: Build and plan shards
script: |
tuist auth login
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5
test-shard-0: &shard-workflow
name: "Shard #0"
instance_type: mac_mini_m2
environment:
xcode: latest
vars:
TUIST_SHARD_INDEX: 0
scripts:
- name: Run shard
script: |
tuist auth login
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'
test-shard-1:
<<: *shard-workflow
name: "Shard #1"
environment:
xcode: latest
vars:
TUIST_SHARD_INDEX: 1
test-shard-2:
<<: *shard-workflow
name: "Shard #2"
environment:
xcode: latest
vars:
TUIST_SHARD_INDEX: 2
test-shard-3:
<<: *shard-workflow
name: "Shard #3"
environment:
xcode: latest
vars:
TUIST_SHARD_INDEX: 3
test-shard-4:
<<: *shard-workflow
name: "Shard #4"
environment:
xcode: latest
vars:
TUIST_SHARD_INDEX: 4

Bitrise#

En Bitrise, Tuist escribe .tuist-shard-matrix.json (incluido shard_plan_id) en el BITRISE_DEPLOY_DIR, poniéndolo a disposición como artefacto de compilación para las etapas posteriores del pipeline. Cada flujo de trabajo de test extrae el ID del plan del artefacto descargado usando envman para que llegue al entorno del comando de test:

yaml
# bitrise.yml
pipelines:
test-pipeline:
stages:
- build-stage: {}
- test-stage: {}
stages:
build-stage:
workflows:
- build-shards: {}
test-stage:
workflows:
- test-shard-0: {}
- test-shard-1: {}
- test-shard-2: {}
- test-shard-3: {}
- test-shard-4: {}
workflows:
build-shards:
steps:
- script:
title: Build and plan shards
inputs:
- content: |
tuist auth login
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5
- deploy-to-bitrise-io: {}
test-shard-0: &shard-workflow
envs:
- TUIST_SHARD_INDEX: 0
steps:
- pull-intermediate-files: {}
- script:
title: Set shard plan ID from artifact
inputs:
- content: |
PLAN_ID=$(jq -r '.shard_plan_id' "$BITRISE_DEPLOY_DIR/.tuist-shard-matrix.json")
envman add --key TUIST_SHARD_PLAN_ID --value "$PLAN_ID"
- script:
title: Run shard
inputs:
- content: |
tuist auth login
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'
test-shard-1:
<<: *shard-workflow
envs:
- TUIST_SHARD_INDEX: 1
test-shard-2:
<<: *shard-workflow
envs:
- TUIST_SHARD_INDEX: 2
test-shard-3:
<<: *shard-workflow
envs:
- TUIST_SHARD_INDEX: 3
test-shard-4:
<<: *shard-workflow
envs:
- TUIST_SHARD_INDEX: 4
Tip

Bitrise no admite la creación dinámica de trabajos paralelos en tiempo de ejecución. Define un número fijo de flujos de trabajo de shard en tus etapas de pipeline: los flujos dentro de una etapa se ejecutan en paralelo automáticamente.

Volúmenes compartidos#

Por defecto, la fase de compilación sube el bundle .xctestproducts al almacenamiento remoto y cada runner de shard lo descarga. Si tu proveedor de CI admite volúmenes compartidos (almacenamiento persistente montado entre trabajos), puedes omitir por completo esta subida/descarga pasando los productos de test a través de un sistema de archivos compartido.

Esto puede reducir significativamente el tiempo de inicio del shard, especialmente para bundles de test grandes.

Para usar volúmenes compartidos:

  1. En la fase de compilación, pasa -testProductsPath apuntando a un volumen compartido y añade --shard-skip-upload para omitir la subida remota:
bash
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5 \
--shard-skip-upload \
-testProductsPath /path/to/shared/volume/$UNIQUE_ID/MyScheme.xctestproducts
  1. En la fase de test, pasa el mismo -testProductsPath para que Tuist lea los productos de test localmente en lugar de descargarlos:
bash
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-testProductsPath /path/to/shared/volume/$UNIQUE_ID/MyScheme.xctestproducts
Important

Usa una ruta única por ejecución de flujo de trabajo (por ejemplo, incluye el ID de ejecución de CI) para evitar colisiones entre ejecuciones concurrentes. También deberías limpiar los productos de test después de completar el sharding para evitar acumular datos obsoletos en el volumen.

FlagVariable de entornoDescripción
--shard-skip-uploadTUIST_TEST_SHARD_SKIP_UPLOADOmitir la subida del bundle de productos de test al almacenamiento remoto

Artefactos autogestionados#

Si tu proveedor de CI ya tiene pasos de subida y descarga de artefactos, puedes dejar que Tuist gestione el archivo y la extracción mientras tu CI gestiona el transporte.

  1. En la fase de compilación, pasa --shard-archive-path para que Tuist escriba su archivo de shard optimizado localmente en lugar de subir los productos de test:
bash
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5 \
--shard-archive-path /tmp/shards/${UNIQUE_ID}/bundle.aar
  1. Sube ese archivo usando el paso nativo de artefactos de tu CI.

  2. En cada trabajo de la fase de test, descarga el archivo y pasa la misma ruta de vuelta a Tuist:

bash
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-archive-path /tmp/shards/${UNIQUE_ID}/bundle.aar

Cuando --shard-archive-path está configurado, Tuist omite la transferencia remota de productos de test y usa el archivo local en su lugar. Si también pasas --shard-skip-upload, la ruta del archivo tiene prioridad.

Namespace#

Los runners de Namespace funcionan bien con los artefactos de GitHub Actions. Configura TUIST_TEST_SHARD_ARCHIVE_PATH una vez para que el trabajo de compilación escriba el archivo del shard localmente, súbelo y descárgalo en cada trabajo del shard antes de ejecutar tuist xcodebuild test:

yaml
name: Tests
on: [pull_request]
env:
TUIST_TEST_SHARD_ARCHIVE_PATH: /tmp/shards/${{ github.run_id }}/bundle.aar
jobs:
build:
name: Build test shards
runs-on: namespace-profile-default-macos
outputs:
matrix: ${{ steps.build.outputs.matrix }}
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
- run: tuist auth login
- id: build
run: |
tuist xcodebuild build-for-testing \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
--shard-total 5
- uses: actions/upload-artifact@v4
with:
name: test-shard-archive
path: ${{ env.TUIST_TEST_SHARD_ARCHIVE_PATH }}
- if: always()
run: rm -rf /tmp/shards/${{ github.run_id }}
test:
name: "Shard #${{ matrix.shard }}"
needs: build
runs-on: namespace-profile-default-macos
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.build.outputs.matrix) }}
env:
TUIST_SHARD_INDEX: ${{ matrix.shard }}
TUIST_SHARD_PLAN_ID: ${{ matrix.shard_plan_id }}
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
- run: tuist auth login
- uses: actions/download-artifact@v4
with:
name: test-shard-archive
path: /tmp/shards/${{ github.run_id }}
- run: |
tuist xcodebuild test \
-scheme MyScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'
- if: always()
run: rm -rf /tmp/shards/${{ github.run_id }}