Pruebas de mutación lo bastante baratas como para ejecutarlas mientras todavía escribes el código.
Rompe tu código a propósito, un operador a la vez, y reporta cada mutación que tus pruebas dejaron pasar. Instala el comando y `ditto staged` lee el índice de git, deduce por su cuenta qué líneas se movieron y te cobra solo por esas. Cada afirmación que hace sobre velocidad viaja con el contador que la refutaría.
┃ Releasing Ditto…┃ calc/calc.go — 7 mutants┃ calc/calc.go:9:12 → Arithmetic┃ baseline: the suite took <duration> on unmutated code, and every mutant runs it again.┃ calc/calc.go:12:11 → Arithmetic┃ calc/calc.go:16:41 → Arithmetic┃ calc/calc.go:4:41 → Comparison┃ calc/calc.go:8:8 → Comparison┃ calc/calc.go:4:40 → Comparison Invert┃ calc/calc.go:8:7 → Comparison Invert┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╍┅┃ 🧬 Survivors┠┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┃ calc/calc.go:12:11 → Arithmetic (- → +)┃ calc/calc.go:16:41 → Arithmetic (+ → -)┃ calc/calc.go:8:8 → Comparison (inserts =)┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╍┅┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╍┅┃ 🧬 Mutant survived: calc/calc.go:12:11 → Arithmetic┠┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┃ --- calc/calc.go (original)┃ +++ calc/calc.go (mutated with 'Arithmetic')┃ @@ -9,7 +9,7 @@┃ return a - b┃ }┃ ┃ - return b - a┃ + return b + a┃ }┃ ┃ // Uncovered is never called, so every mutant of it lives.┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╍┅ … two further survivor diffs elided … ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓┃ • Total: 7 ┃┃ • Killed: 4 ┃┃ • Survived: 3 ┃┠┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┨┃ ✓ Score: 0.57 (minimum: 0.00) ┃┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛Arrancar el comando de pruebas cuesta entre 750 y 950 milisegundos por mutante, haga lo que haga la suite, y ese peaje domina la ejecución. Al compilar una sola vez y elegir el mutante en tiempo de ejecución, se paga una vez para toda la ejecución: los mismos doce mutantes vuelven en 0.4 a 1.3 segundos frente a 10 u 11. Los dos mecanismos de abajo recortan cuántas veces se invoca el comando.
WithChangedRangesPaga por las líneas que el cambio realmente tocó
Entrega los rangos de bytes que cubre un cambio, indexados por archivo, y la release muta solo esos. O ejecuta `ditto staged`, que los lee por ti del índice de git. Sobre el fixture de referencia, una función modificada cobra los cuatro operadores que disparan en esa línea y nada más del repositorio.
Escala con tu cambio: una función modificada en cada uno de dos archivos cobra ocho ejecuciones, exactamente el doble de lo que cobra un archivo, porque a cada archivo solo se le cobran sus propios rangos.
GatedUna sola compilación sostiene todos los mutantes del archivo
El archivo se instrumenta para que cada mutante sea una compuerta elegida en tiempo de ejecución, el paquete se compila una sola vez con `go test -c` y cada mutante se selecciona por variable de entorno. Un archivo que la instrumentación no puede sostener vuelve solo a la ruta ordinaria, y la ejecución sigue sobre el resto del repositorio.
Donde las dos rutas difieren, la de compuertas es la que tiene razón: puntúa un mutante que la compilación ordinaria no puede construir.
Gated| Veredicto | Ordinaria | Compuertas |
|---|---|---|
| Muertos | 127 | 127 |
| Supervivientes | 8 | 8 |
Cuánto vale la ganancia depende de tu suite: el mismo mecanismo produjo 2.7× en un paquete cuya suite tarda 243 ms y 1.5× en uno que tarda 1155 ms, porque el peaje que elimina es un costo fijo.
ditto lee el destino de un mutante a partir de un comando de pruebas que falla. Si la suite ya está roja, todos los mutantes parecen muertos y la ejecución reporta un puntaje perfecto. Todo lo que sigue es lo que se interpone entre ese número y tú.
Una ejecución base de tu comando sobre el código sin mutar abre cada release, y si sale en rojo la termina ahí, con la salida del propio comando, así que puedes leer el motivo sin entrar al sandbox. Esa ejecución además anuncia cuánto costó la suite, que es el precio que cada mutante vuelve a pagar.
Ejecuta el archivo instrumentado sin ningún mutante seleccionado, que es la suite propia del archivo: una medición ya pagada, y ahora leída. Sobre los mismos mutantes separa un reporte de 4 muertos de 4 del verdadero 1 de 4, y esa lectura es gratis.
`internal/verdict` lo lee del flujo de eventos de `go test -json` que la ejecución ya produce, y los cuatro motivos que puede devolver son los que se dibujan al inicio de esta sección.
Un mutante que nunca llegó a ser un programa sale del puntaje por completo, del numerador y del denominador, así que lo que lees es la proporción de mutantes ejecutables que tus pruebas atraparon. Sobre un fixture donde dos de cinco mutantes no compilaron, eso es 0.33.
Cuando las compuertas están activas y un archivo no sostiene el esquema, ese archivo vuelve solo a la ruta ordinaria y la ejecución sigue sobre todo lo demás. En ditto mismo exactamente un archivo lo necesitó, y el resto conservó su única compilación.
`ruta:línea:columna → Operador (lo que reemplazó)`, listado antes de cualquier diff, para poder saltar directo a un superviviente. Es además lo que distingue a dos supervivientes: sobre 135 mutantes de cuatro archivos con gofmt, 129 son indistinguibles de otro mutante en todo salvo en su dirección y el texto que reemplazaron.
Una prueba golden compara una release completa contra un fixture que tiene tanto muertes como supervivientes, en la ruta ordinaria y en la gated, así que a las dos se les exige imprimir lo mismo.
Cada número de abajo es un entero exacto: un conteo de análisis, sandboxes, archivos e invocaciones del comando de pruebas, idéntico en cualquier máquina. Cada uno está fijado, y el build falla si se mueve en cualquier dirección, así que lo que una release cuesta hoy es lo que te costará la próxima semana.
| Contador | Era | Es |
|---|---|---|
sourceParsesPerReleaseWithThreeVirusesUn análisis sintáctico por archivo fuente, compartido entre todos los operadores: un archivo se lee una vez, sin importar cuántos de los catorce operadores por defecto estén activos. | 12 | 4 |
sandboxesBuiltPerReleaseLos sandboxes se agrupan y el archivo mutado se restaura antes de devolver uno, así que una ejecución secuencial construye exactamente uno y una paralela construye tantos como su concurrencia máxima. | 48 | 1 |
filesLinkedPerSandboxSeis archivos de trabajo: un sandbox tiene el árbol sin `.git`. El conteo es el mismo sin importar cómo llega cada archivo (copiado, con enlace duro o con enlace simbólico), a unos 0.45 ms por archivo, y se paga una vez por release. | 11 | 6 |
laboratoryRunsForOneChangedFunctionCuatro ejecuciones por una función modificada: los operadores que disparan en esa línea y nada más del repositorio. | 48 | 4 |
laboratoryRunsForOneChangedFunctionInEachOfTwoFilesUna función modificada en cada uno de dos archivos cuesta exactamente el doble que una, porque a cada archivo se le cobran solo sus propios rangos. | 48 | 8 |
mutantsPerReleaseOnThisRepositoryLo que paga una ejecución completa sobre el propio repositorio de ditto: 785 mutantes. Se mide contra el repositorio mismo, así que es la cifra que dice cuánto cuesta una ejecución de ese tamaño. | 431 | 785 |
testCommandInvocationsPerReleaseWholeFixtureCuarenta y ocho mutantes, más la única ejecución base de tu suite sobre el código sin mutar que hace que el puntaje signifique algo. Una base para toda la release. | 48 | 49 |
El tiempo de reloj se mide y se reporta: en una máquina de desarrollo real, la misma carga de trabajo varió aquí más de un cincuenta por ciento entre ejecuciones, mientras que los conteos de arriba se mantuvieron exactos.
Cada uno edita el árbol sintáctico como lo haría un error real: un operador invertido, una constante desplazada, un bucle interrumpido. Catorce están activos antes de que configures nada, desde el comando o desde `Release`; el decimoquinto se agrega nombrándolo.
+↔-*↔/%↔*+=-=*=/=%=&=|=^=<<=>>=&^=→=+=↔-=*=↔/=%=↔*=&↔|^→&&^→&<<↔>><↔<=>↔>=>↔<=<↔>===↔!=&&operand→true||operand→falsex→x-1.0x→x+1.0n→n-1n→n+1break↔continuecondition→falserange→earlybreakcontext.CancelCauseFunc(err)→(nil)Un virus es cualquier struct que satisfaga la interfaz `viruses.Virus`, así que una mutación que solo significa algo en tu código es un tipo y una llamada a `WithViruses`. El paquete `dittotesting` trae los ayudantes con los que se prueban los quince que vienen incluidos, para que el tuyo reciba el mismo trato.
El comando es la forma más corta de empezar: instálalo una vez y `ditto staged` muta solo lo que justifica tu cambio preparado, con `--dry` para cotizarlo antes de pagarlo. La librería es el mismo motor desde un binario de pruebas que ya tienes.
Cada mutante se juzga con el comando de pruebas de abajo, y `-json` es lo que permite que ditto diga por qué murió.
$ go install github.com/Disble/ditto/cmd/ditto@latest$ ditto staged --threshold 0.8$ go test -count=1 -json ./...Una importación y una etiqueta de compilación ponen el mismo motor dentro de la suite que ya ejecutas, y con `WithTestCommand` indicas otro comando de pruebas.
$ go get github.com/Disble/ditto$ go test -v -tags=mutation$ go test -count=1 -json ./...ditto — mutation testing for Go ditto run [flags] mutate a repository and report what survived ditto staged [flags] mutate only what a staged change justifies ditto changed [flags] mutate only what a committed change justifies ditto version the module version this binary was built from Run `ditto run -h` for its flags.//go:build mutation package main_test import ( "testing" "github.com/Disble/ditto") func TestMutation(t *testing.T) { ditto.Release(t)}El código, la bitácora fechada y la línea base de rendimiento fijada.
Cada opción, cada operador, la interfaz `viruses.Virus` y los puntos de entrada `RunStaged` y `RunChanged`, generados desde el código.
La puerta de commit que entrega sus propios cambios de Go preparados directamente a `ditto staged`.
ditto es un fork de gtramontina/ooze, de Guilherme J. Tramontina. Todas las buenas ideas de aquí son suyas, y la licencia y el copyright siguen siendo de él; es MIT, igual que el original.