Otros sistemas de CI
TestDetta funciona en cualquier CI que ejecute .NET y git. Fuera de GitHub Actions, bastan dos comandos y una carpeta.
- En la rama predeterminada,
td record --maps-dir <maps>ejecuta una vez todo el conjunto de pruebas con cobertura y guarda un mapa de cobertura por proyecto de pruebas en<maps>/<commit>/. - En una pull request o merge request,
td test --base origin/<target> --maps-dir <maps>restaura el mapa más reciente en la merge base o anterior a ella. Toma el más cercano dentro de 50 commits de primer padre (--max-map-age) y salva los cambios intermedios. Después ejecuta solo las pruebas afectadas.
<maps> es una carpeta normal. Guárdala donde tu CI pueda conservar archivos entre pipelines:
- un disco compartido por los agentes, habitual on-premises:
/srv/testdetta-maps/<repository>; - almacenamiento de objetos (S3, Azure Blob, GCS, MinIO), sincronizado antes y después.
TestDetta en sí nunca usa la red para los mapas.
Qué necesita cualquier configuración
- El historial de git completo. TestDetta compara con la merge base y recorre el historial para encontrar mapas. Los clones superficiales (shallow) no permiten ninguna de las dos cosas.
- La rama de destino descargada, para que exista
origin/<target>. Descárgala con un refspec explícito,+refs/heads/<target>:refs/remotes/origin/<target>. Los clones de CI suelen limitar los refspecs del remoto a la propia rama del pipeline (los de GitLab y Bitbucket lo hacen), y entoncesgit fetch origin <target>por sí solo actualiza únicamenteFETCH_HEAD, yorigin/<target>sigue sin existir. - La licencia en
TESTDETTA_LICENSE(una variable enmascarada o secreta) o en un archivo indicado porTESTDETTA_LICENSE_FILE. Los repositorios públicos no la necesitan en GitLab CI ni en GitHub Actions; Jenkins, Azure DevOps y Bitbucket no indican si un repositorio es público, así que allí todos los repositorios la necesitan. Sin una licencia válida se ejecutan todas las pruebas, y la compilación nunca falla por ello. Consulta Licencia. - Códigos de salida:
0éxito,1el análisis falló,2línea de comandos no válida,3fallaron pruebas. - Limpieza: los mapas de commits antiguos solo se necesitan durante 50 commits. Elimina las carpetas más antiguas con un job programado o una regla de ciclo de vida del almacenamiento.
Instalación
dotnet tool install --global <package>
El nombre del paquete se fijará en el lanzamiento. Hasta entonces, compila TestDetta desde el código fuente con dotnet build src/TestDetta.Cli -c Release -o testdetta-bin y llama a dotnet testdetta-bin/TestDetta.Cli.dll allí donde aparezca td más abajo.
GitLab CI
variables:
GIT_DEPTH: "0" # full history
TESTDETTA_MAPS: /srv/testdetta-maps/$CI_PROJECT_PATH # a disk the runners share; or sync with object storage
record-coverage:
stage: test
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- td record --maps-dir "$TESTDETTA_MAPS"
affected-tests:
stage: test
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- git fetch --no-tags origin "+refs/heads/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME:refs/remotes/origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
- td test --base "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" --maps-dir "$TESTDETTA_MAPS" --summary-file testdetta-summary.md
artifacts:
when: always
paths: [testdetta-summary.md]
Define TESTDETTA_LICENSE en Settings → CI/CD → Variables, enmascarada y protegida según sea necesario. TestDetta detecta los proyectos públicos mediante CI_PROJECT_VISIBILITY.
Jenkins
Un pipeline multibranch declarativo:
pipeline {
agent any
environment {
TESTDETTA_LICENSE = credentials('testdetta-license') // a "secret text" credential
TESTDETTA_MAPS = "/srv/testdetta-maps/${env.JOB_NAME.split('/')[0]}"
}
options { skipDefaultCheckout() }
stages {
stage('Checkout') {
steps {
checkout scmGit(branches: scm.branches, userRemoteConfigs: scm.userRemoteConfigs,
extensions: [cloneOption(shallow: false, depth: 0, noTags: true, reference: '')])
}
}
stage('Record coverage') {
when { branch 'main' }
steps { sh 'td record --maps-dir "$TESTDETTA_MAPS"' }
}
stage('Affected tests') {
when { changeRequest() }
steps {
sh 'git fetch --no-tags origin "+refs/heads/$CHANGE_TARGET:refs/remotes/origin/$CHANGE_TARGET"'
sh 'td test --base "origin/$CHANGE_TARGET" --maps-dir "$TESTDETTA_MAPS"'
}
}
}
}
Jenkins suele clonar con credenciales en la URL del remoto. TestDetta las elimina antes de comparar con el ámbito de la licencia y nunca las registra. Si el remoto no nombra el repositorio que cubre la licencia (por ejemplo, un mirror local), define TESTDETTA_LICENSE_SCOPE, como git.acme.corp/scm/pay/app.
Azure DevOps Pipelines
trigger: [main]
pr: [main]
steps:
- checkout: self
fetchDepth: 0
- script: td record --maps-dir "$(TESTDETTA_MAPS)"
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
env:
TESTDETTA_LICENSE: $(TestDettaLicense) # a secret pipeline variable
- script: |
target="${SYSTEM_PULLREQUEST_TARGETBRANCH#refs/heads/}"
git fetch --no-tags origin "+refs/heads/$target:refs/remotes/origin/$target"
td test --base "origin/$target" --maps-dir "$(TESTDETTA_MAPS)" --summary-file "$(Agent.TempDirectory)/testdetta.md"
echo "##vso[task.uploadsummary]$(Agent.TempDirectory)/testdetta.md"
condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
env:
TESTDETTA_LICENSE: $(TestDettaLicense)
TESTDETTA_MAPS apunta a un recurso compartido en los agentes autohospedados. En los agentes hospedados por Microsoft, sincronízala con Azure Blob Storage (consulta Almacenamiento de objetos).
Bitbucket Pipelines
clone:
depth: full
pipelines:
branches:
main:
- step:
script:
- td record --maps-dir maps
- ./sync-maps.sh upload maps # see "Object storage"
pull-requests:
'**':
- step:
script:
- git fetch --no-tags origin "+refs/heads/$BITBUCKET_PR_DESTINATION_BRANCH:refs/remotes/origin/$BITBUCKET_PR_DESTINATION_BRANCH"
- ./sync-maps.sh download maps "origin/$BITBUCKET_PR_DESTINATION_BRANCH"
- td test --base "origin/$BITBUCKET_PR_DESTINATION_BRANCH" --maps-dir maps
Define TESTDETTA_LICENSE como variable de repositorio protegida (secured).
Almacenamiento de objetos en lugar de un disco compartido
En los runners hospedados no hay disco compartido. Sube la carpeta nueva después del registro. Antes de las pruebas, descarga solo las carpetas que se pueden usar: la merge base y hasta 50 commits de primer padre anteriores a ella. Descárgalas de la más reciente a la más antigua y detente en la primera que exista. Un ejemplo para S3; cualquier herramienta con cp y ls funciona igual:
#!/usr/bin/env bash
# sync-maps.sh upload <maps> | after td record on the default branch
# sync-maps.sh download <maps> <base> | before td test on a pull request
set -euo pipefail
remote="s3://my-bucket/testdetta-maps/my-repository"
case "$1" in
upload)
aws s3 cp --recursive "$2/$(git rev-parse HEAD)" "$remote/$(git rev-parse HEAD)" ;;
download)
for sha in $(git rev-list --first-parent -n 51 "$(git merge-base "$3" HEAD)"); do
if aws s3 ls "$remote/$sha/" > /dev/null 2>&1; then
aws s3 cp --recursive "$remote/$sha" "$2/$sha"
break
fi
done ;;
esac
Cuando no hay mapa
Las primeras pull requests tras la configuración, o las que están muy atrasadas, no encuentran ningún mapa a su alcance. TestDetta selecciona entonces solo por nombre, lo cual es seguro pero selecciona más, y lo indica en su registro. La selección se vuelve precisa en cuanto td record se ha ejecutado en la rama predeterminada.