Como mencionamos anteriormente, el conjunto de candidatos a reparación a tener en cuenta, debe ser finito y su tamaño afecta directamente a los recursos necesarios para reparar el programa. Esto genera una necesidad de balancear capacidad de reparación, es decir, que tipo de fallas se pueden reparar, con recursos necesarios. Por ejemplo, el caso deGenProg, se basa en mover, duplicar, o eliminar código. La idea es que muchas veces un desarrollador escribe varias veces el mismo código, por ejemplo, sumarle 1 a una variable. Por otro lado, en SPR [Long and Rinard, 2015], se generan reparaciónes abstractas como agregar una condición C antes de la ejecución de una sentencia, la cual luego será reemplazada por una condición concreta en una etapa posterior, esto sumado a la eliminación, y duplicación de código existente. En PAR [Kim et al., 2013], las modificaciones para reparar el programa son aprendidas a partir de patrones en reparaciones escritas manualmente. Por esto, el número de candidatos a 2Algunas herramientas no soportaban ciertas características (retornar una cadena), y en otras
7.1 reparación automática de programas 127 boolean add ( O b j e c t a r g ) { S L i s t N o d e f r e s h N o d e = new S L i s t N o d e ( ) ; f r e s h N o d e . v a l u e = a r g ; boolean added = f a l s e; i f (t h i s. h e a d e r == n u l l) { t h i s. h e a d e r = f r e s h N o d e ; added = true; } e l s e { S L i s t N o d e c u r r e n t = t h i s. h e a d e r . n e x t ; //BUG: i g n o r a p r i m e r nodo while ( c u r r e n t . n e x t ! = n u l l && c u r r e n t . v a l u e ! = a r g ) { c u r r e n t = c u r r e n t . n e x t ; } i f ( c u r r e n t . v a l u e ! = a r g ) { c u r r e n t . n e x t = f r e s h N o d e ; added = true; } } i f ( added ) { s i z e = s i z e − 1 ; //BUG: d e c r e m e n t a s i z e } return added ; }
Figura 60: Ejemplo de código con dos bugs que requieren modificaciones intra- sentencia
considerar como reparaciones es significativamente reducido, lo que a su vez reduce el tipo de errores que la técnica puede ser capaz de reparar.
Modificaciones a partes de sentencias, es decir, aquellas que alteran expre- siones dentro de una sentencia, son en general no consideradas por técnicas de reparación de programas. Una limitación principal al considerar a éstas, es la explosión en el espacio de candidatos de reparación. Técnicas que utilizan operadores de mutación para producir las modificaciones sintácticas, y que incluyen modificaciones a partes de sentencias, requieren limitar el conjunto de mutaciones (por ejemplo, [Gopinath et al., 2011]), reduciendo la clase de fallas que éstas pueden intentar reparar.
Los efectos que tienen estas restricciones sobre el espacio de candidatos a considerar para la reparación, pueden observarse en la Figura 60 donde se muestra el método add(Object) de un conjunto. Éste contiene dos defectos: primero, cuando el conjunto no está vació, se comienza el recorrido del mismo a partir del nodo siguiente al inicial (el cual podría sernull, resultando en un error en la línea siguiente); segundo, cuando un nuevo nodo es agregado a la lista sobre la cual está implementado el conjunto, el tamaño de la misma (atributo size) se decrementa. Para reparar al método es necesario realizar dos cambios,
7.1 reparación automática de programas 128
ambos dentro de una sentencia. En herramientas que hacen cambios a nivel bloque, solo es posible reparar el programa si existen en otro punto del código las sentencias current = this.header; ysize = size + 1.
Mutación en reparación
El uso de operadores de mutación, provenientes de mutation testing, en reparación de programas, parece razonable: si estos operadores son utilizados en mutation testing para emular defectos reales, podrían utilizarse para emular reparaciones. Ejemplos recientes de herramientas de reparación automática de programas son [Debroy and Wong, 2010] y [Wang et al., 2018a]. El argumento de utilizar mutación en reparación de programas es que existen mutaciones que si se fueran a combinar, se cancelarían, por ejemplo:
f o r (i n t i = 0 ; i < l e n g h t ; i + + ) . . .
∆f o r (i n t i = 0 ; i > l e n g h t ; i + + ) . . .
donde la primera sentencia se puede mutar, aplicando un cambio de operador relacional, a la segunda, marcada por ∆, que a su vez se puede mutar al código original aplicando el mismo operador de mutación. Aunque no siempre se puede deshacer una mutación aplicando otra que sea sintácticamente inversa. Volviendo al ejemplo anterior, el mutante:
f o r (i n t i = 0 ; i > l e n g h t ; i + + ) . . .
puede ser restaurado, semánticamente, generando el mutante:
f o r (i n t i = 0 ; i != l e n g h t ; i + + ) . . .
Existen también casos donde varias mutaciones pueden corregir el comporta- miento. Esto lleva a la idea de que si consideramos el programa con fallasPb y el original sin fallasPo, se puede definir el segundo en términos del primero como
Pb = mutate(Po, M)dondeMrepresenta una secuencia de mutaciones ymutate es un programa que aplica dicha secuencia a un programa. En general como dijimos anteriormente, muchas mutaciones tienen su inversa, ya sea sintáctica o semántica, lo que lleva a definir el problema de reparación como encontrar una secuencia de mutaciones M0tal quePo = mutate(Pb, M0).
La reparación de programas puede describirse como una búsqueda exhaustiva que, dado un programa defectuoso y una especificación del mismo (que como vimos puede ser formal o mediante tests), y un conjunto de operadores de mutación:
7.1 reparación automática de programas 129 return a < b?a:b; return b < b?a:b; return a < b?a:0; return b < a?a:b; a -> b b -> 0 b -> a Inicial Candidatos (Profundidad 1) Exitoso (Profundidad 2)
Figura 61: Reparación de un programa que calcula el máximo de dos números Tabla 30: Mutantes generados para getNode de la Figura 62, considerando 4 líneas y 18 operadores, a medida que la profundidad aumenta.
Search Depth No. of Mutants (Fix Candidates)
1 40
2 1,604
3 64,684
4 >20 million
2. Si P es un candidato a reparación, y Q es el resultado de aplicar una mutación aP, entoncesQ también es un candidato a reparación.
3. Un candidatoS es exitoso si satisface las especificaciones provistas.
La definición previa de reparación automática de programas deja en claro que el espacio de candidatos a reparación depende del número de mutaciones (b) a considerar, lo que influye en cuan “ancho” es el árbol de búsqueda, y el número máximo de mutaciones sucesivas permitidas (d) para generar a los candidatos, lo que afecta la profundidad de la búsqueda. El espacio de búsqueda queda entonces definido por una suma geométrica bdb+−1−11 (O(bd)). Si consideramos el método en la Figura62, y solo cuatro líneas del mismo, al utilizar 18 operadores de mutación (mediante la herramienta µJava++), podemos ver en la Tabla30, como a medida que aumentamos la cantidad de mutaciones consecutivas (la profundidad en reparación mediante mutación), la cantidad de candidatos a evaluar se vuelve rápidamente inmanejable.
7.1 reparación automática de programas 130 Node getNode (i n t i ) { Node c u r r e n t = t h i s. head ; Node r e s u l t = n u l l; i n t c u r r e n t _ i n d e x = 0 ; while ( r e s u l t == n u l l && c u r r e n t != n u l l) { i f ( i == c u r r e n t _ i n d e x ) { r e s u l t = c u r r e n t ; } c u r r e n t _ i n d e x = c u r r e n t _ i n d e x + 1 ; c u r r e n t = c u r r e n t . n e x t ; } return r e s u l t ; }
Figura 62: Código de ejemplo, método que obtiene el i-ésimo nodo de una lista