Operadores de mutação desse grupo fazem alterações em transformações especí- ficas. Essas alterações incluem a substituição de uma transformação por outra, inclusão ou exclusão de uma transformação e alterações nos argumentos de entrada de uma trans- formação. Ao contrário dos operadores de mutação para fluxo de dados, os operadores desse grupo fazem alterações em uma transformação específica sem considerar as demais transformações do programa.
Para a definição de alguns operadores de mutação, vamos considerar uma trans- formação especial denominada transformação identidade, do tipo 𝐵𝑎𝑔[𝛼] → 𝐵𝑎𝑔[𝛼] que recebe um conjunto de dados 𝑑 do tipo 𝐵𝑎𝑔[𝛼] como entrada e retorna o próprio conjunto 𝑑 como saída (𝑖𝑑𝑒𝑛𝑡𝑖𝑡𝑦(𝑑) = 𝑑). Essa transformação identidade é utilizada nos casos em que precisamos representar a remoção de uma transformação. Os operadores de mutação de remoção que são apresentados nessa seção (FTD, DTD e OTD) são casos específicos do operador de remoção de transformação unária (UTD). Logo, quando o operador de mutação UTD for aplicado no programa, a aplicação de FTD, DTD ou OTD não é ne- cessária porque esses gerariam mutantes que já seriam criados com o UTD. Mantivemos a definição desses três operadores de mutação porque esses representam situações mais específicas dentre os defeitos que estamos simulando com base na taxonomia de defeitos funcionais para o Apache Spark apresentada no Capítulo 5. Por exemplo, um defeito no tratamento de dados duplicados (F2.8) pode ser representado com o operador de remoção de transformação distinct (DTD), enquanto que o operador UTD representa um defeito mais genérico da ausência de uma transformação no fluxo de dados (F1). Assim sendo, os operadores FTD, DTD ou OTD podem ser aplicados em situações mais específicas em que não está sendo aplicado o operador UTD. Isso fica a critério do engenheiro de testes que pode selecionar apenas alguns operadores para aplicar no processo de teste de mutação.
formation Replacement): para cada transformação de mapeamento (map e flatMap)
existente no programa, que recebe uma função de mapeamento 𝑓 do tipo 𝛼 → 𝛽 ou 𝛼 → 𝐵𝑎𝑔[𝛽], este operador de mutação gera um mutante substituindo-se a função 𝑓 na transformação por uma função de mapeamento arbitrária 𝑓𝑚 do mesmo tipo de 𝑓 . De forma intuitiva, para cada transformação de mapeamento no programa, esse operador de mutação gera um mutante em que substitui a função de mapeamento passada como argumento por uma função de mapeamento diferente.
Uma função 𝑓𝑚 pode ser definida para uma transformação específica ou para um tipo específico a critério do engenheiro de testes. Nós propomos uma função de mapeamento que retorna um valor constante do mesmo tipo de retorno da função original ou que faz alguma modificação no valor retornado pela função original. Para a definição desses valores, nos inspiramos nos critérios de teste de análise do valor limite e classes de equivalência (AMMANN; OFFUTT, 2017). Nesses critérios, os domínios de entrada ou saída de um programa ou função são divididos em diferentes categorias, chamadas classes de equivalência. Essas classes de equivalência representam valores válidos ou inválidos dentro do domínio, assim como valores incomuns ou que exploram limites, como um valor máximo ou mínimo aceito para um tipo específico, por exemplo. Baseado nesses critérios de teste, a função de mapeamento que estamos propondo retorna valores típicos e atípicos, assim como valores limites, com base nos tipos de retorno da função de mapeamento. Os tipos que estamos considerando são os tipos numéricos (Integer, Float, Double e Long), lógicos (Boolean), textos (String) e tipos de coleções (List, Array e Set). A Tabela 6.2 apresenta os valores de retorno da função que estamos propondo com base no seu tipo de retorno. Nessa tabela, o 𝑥 representa o valor retornado pela função original, o 𝑘 e 𝑣 representam a chave e o valor, respectivamente, retornados pela função original quando essa função retornar uma tupla, e o 𝑘𝑚 e 𝑣𝑚 representam valores modificados para a chave e o valor, respectivamente, que é a aplicação de outros valores de mapeamento respeitando os seus tipos.
Tabela 6.2 – Valores de mapeamento para tipos básicos e coleções.
Tipo Valor de Mapeamento
Numérico 0, 1, 𝑀 𝐴𝑋, 𝑀 𝐼𝑁, −𝑥 Lógico 𝑡𝑟𝑢𝑒, 𝑓 𝑎𝑙𝑠𝑒, ¬𝑥 Texto “ ” Coleções 𝐿𝑖𝑠𝑡(𝑥.ℎ𝑒𝑎𝑑), 𝑥.𝑡𝑎𝑖𝑙, 𝑥.𝑟𝑒𝑣𝑒𝑟𝑠𝑒, 𝑁 𝑖𝑙 Tupla (𝑘𝑚, 𝑣) , (𝑘, 𝑣𝑚) Geral null
Para exemplificar esse operador de mutação, vamos considerar que no programa existe uma transformação de mapeamento 𝑡1 que aplica uma função 𝑓 que mapeia um
valor numérico para o seu sucessor:
𝑡1 = 𝑚𝑎𝑝(𝑓 (𝑥) = 𝑥 + 1, 𝑑)
Ao aplicar o operador MTR nessa transformação, geramos cinco mutantes uma vez que definimos cinco valores de mapeamento para tipos numéricos de acordo com a Tabela 6.2: 𝑡1 = 𝑚𝑎𝑝(𝑓𝑚(𝑥) = 0, 𝑑) 𝑡1 = 𝑚𝑎𝑝(𝑓𝑚(𝑥) = 1, 𝑑) 𝑡1 = 𝑚𝑎𝑝(𝑓𝑚(𝑥) = 𝑀 𝐴𝑋, 𝑑) 𝑡1 = 𝑚𝑎𝑝(𝑓(𝑥) = 𝑀 𝐼𝑁, 𝑑) 𝑡1 = 𝑚𝑎𝑝(𝑓𝑚(𝑥) = −(𝑥 + 1), 𝑑)
FTD - Remoção de Transformação de Filtragem (Filter Transformation De- letion): para cada transformação de filtragem (𝑓 𝑖𝑙𝑡𝑒𝑟) existente no programa, este ope-
rador gera um mutante em que essa transformação é substituída pela transformação iden- tidade do conjunto de dados de entrada da transformação. Em outras palavras, esse ope- rador de mutação remove a chamada de uma transformação de filtragem no programa, sendo uma especialização do operador UTR. Por exemplo, considere que no programa existe uma transformação de filtragem que remove os valores numéricos negativos do conjunto de dados:
𝑡1 = 𝑓 𝑖𝑙𝑡𝑒𝑟(𝑝(𝑥) = 𝑥 ≥ 0, 𝑑)
Então, a aplicação do operador FTD vai gerar o seguinte mutante: 𝑡1 = 𝑖𝑑𝑒𝑛𝑡𝑖𝑡𝑦(𝑑)
NFTP - Negação do Predicado de Transformação de Filtragem (Negation of Filter Transformation Predicate): para cada transformação de filtragem (𝑓 𝑖𝑙𝑡𝑒𝑟)
existente no programa que recebe uma função de predicado 𝑝 do tipo 𝛼 → 𝑏𝑜𝑜𝑙𝑒𝑎𝑛, este operador gera um mutante em que 𝑝 é substituído por uma função de predicado 𝑝𝑚 que nega o resultado de 𝑝. Por exemplo, considere a mesma transformação de filtragem que foi utilizada como exemplo para o operador FTD. Ao aplicar o operador NFTP, vamos gerar o seguinte mutante:
𝑡1 = 𝑓 𝑖𝑙𝑡𝑒𝑟(𝑝𝑚(𝑥) = ¬(𝑥 ≥ 0), 𝑑)
STR - Substituição de Transformação de Conjuntos (Set Transformation Replacement): para cada ocorrência de uma transformação de conjuntos (union, in-
dois mutantes em que a transformação é substituída por cada uma das outras duas trans- formações de conjuntos restantes; (3) um mutante em que a transformação é substituída pela transformação identidade do primeiro conjunto de dados de entrada da transforma- ção; (4) um mutante em que a transformação é substituída pela transformação identidade do segundo conjunto de dados de entrada da transformação; e (5) um mutante em que a ordem dos dois conjuntos de dados de entrada da transformação original é invertida. Considerando que as transformações union e intersection são comutativas, esta última substituição irá gerar um mutante equivalente, portanto, esta substituição deve ser feita apenas quando a transformação for a subtract. Para exemplificar, vamos considerar que no programa existe a transformação 𝑡1 = 𝑠𝑢𝑏𝑡𝑟𝑎𝑐𝑡(𝑑1, 𝑑2), a aplicação deste operador de mutação gera os seguintes mutantes:
𝑡1 = 𝑢𝑛𝑖𝑜𝑛(𝑑1, 𝑑2) 𝑡1 = 𝑖𝑛𝑡𝑒𝑟𝑠𝑒𝑐𝑡𝑖𝑜𝑛(𝑑1, 𝑑2) 𝑡1 = 𝑖𝑑𝑒𝑛𝑡𝑖𝑡𝑦(𝑑1)
𝑡1 = 𝑖𝑑𝑒𝑛𝑡𝑖𝑡𝑦(𝑑2) 𝑡1 = 𝑠𝑢𝑏𝑡𝑟𝑎𝑐𝑡(𝑑2, 𝑑1)
DTD - Remoção de Transformação Distinct (Distinct Transformation De- letion): para cada transformação distinct existente no programa, este operador gera
um mutante em que essa transformação é substituída pela transformação identidade do conjunto de dados de entrada da transformação. De forma intuitiva, este operador re- move a chamada da transformação distinct do programa e também é uma especialização do operador UTD. Por exemplo, considerando que no programa existe a transformação 𝑡1 = 𝑑𝑖𝑠𝑡𝑖𝑛𝑐𝑡(𝑑), a aplicação do operador DTD gera o mutante:
𝑡1 = 𝑖𝑑𝑒𝑛𝑡𝑖𝑡𝑦(𝑑)
DTI - Inserção de Transformação Distinct (Distinct Transformation Inser- tion): para cada transformação diferente de distinct no programa, esse operador gera um
mutante em que a transformação distinct é inserida logo após a chamada dessa transfor- mação. Essa mutação implica em alterações no fluxo de dados para ajustar a aplicação da transformação distinct logo após a transformação original e antes da transformação seguinte que recebia o conjuntos de dados resultante da transformação original.
Para ilustrar esse operador de mutação, vamos considerar a representação gráfica do fluxo de dados de um programa apresentado na Figura 6.10. Considerando que a transformação 𝑡1 não é uma transformação distinct, a aplicação do operador DTI gera o mutante representado na Figura 6.11. Nesse mutante, a transformação distinct foi inserida logo após a transformação 𝑡1, recebendo o conjunto de dados resultante de 𝑡1como entrada. O conjunto de dados resultante de distinct passa a ser a entrada para a transformação
𝑡2, que no programa original era executada logo após 𝑡1. Para esse programa, também seria gerado um mutante em que a transformação distinct seria inserida logo após a transformação 𝑡2, caso essa também não fosse uma transformação distinct.
t1 t2
Figura 6.10 – Representação gráfica do fluxo de dados de um programa.
t1 distinct t2
Figura 6.11 – Mutante gerado com o operador de mutação DTI.
ATR - Substituição de Transformação de Agregação (Aggregation Transfor- mation Replacement): para cada transformação de agregação (reduce e reduceByKey)
existente no programa, que recebe uma função de agregação 𝑓 como argumento de en- trada, esse operador de mutação gera um mutante em que 𝑓 é substituída por outra função de agregação 𝑓𝑚 do mesmo tipo. Uma função 𝑓𝑚 pode ser definida para uma transformação específica ou para um tipo específico a critério do engenheiro de testes.
Para simular defeitos relacionados com as referências dos parâmetros de entrada na função de agregação, estamos propondo cinco funções de agregação para substituir a função original 𝑓 : (1) uma função que retorna o primeiro parâmetro de entrada (𝑓𝑚(𝑥, 𝑦) = 𝑥); (2) uma função que retorna o segundo parâmetro de entrada (𝑓𝑚(𝑥, 𝑦) = 𝑦); (3) uma função que ignora o segundo parâmetro de entrada e aplica a função original 𝑓 com o primeiro parâmetro de entrada duplicado (𝑓𝑚(𝑥, 𝑦) = 𝑓 (𝑥, 𝑥)); (4) uma função que ignora o primeiro parâmetro de entrada e aplica a função original 𝑓 com o segundo parâmetro de entrada duplicado (𝑓𝑚(𝑥, 𝑦) = 𝑓 (𝑦, 𝑦)); e, por último, (5) uma função que inverte a ordem dos parâmetros de entrada na função original 𝑓 (𝑓𝑚(𝑥, 𝑦) = 𝑓 (𝑦, 𝑥)). Esta última função de agregação gera um valor diferente quando a função 𝑓 não for comutativa. Para ilustrar a aplicação do operador de mutação ATR, vamos considerar que o programa possui uma transformação de agregação que aplica uma função 𝑓 para obter o maior valor do conjunto de dados:
A aplicação do operador ATR irá gerar os seguintes mutantes: 𝑡1 = 𝑟𝑒𝑑𝑢𝑐𝑒(𝑓𝑚(𝑥, 𝑦) = 𝑥, 𝑑) 𝑡1 = 𝑟𝑒𝑑𝑢𝑐𝑒(𝑓𝑚(𝑥, 𝑦) = 𝑦, 𝑑) 𝑡1 = 𝑟𝑒𝑑𝑢𝑐𝑒(𝑓𝑚(𝑥, 𝑦) = if 𝑥 > 𝑥 then 𝑥 else 𝑥, 𝑑) 𝑡1 = 𝑟𝑒𝑑𝑢𝑐𝑒(𝑓𝑚(𝑥, 𝑦) = if 𝑦 > 𝑦 then 𝑦 else 𝑦, 𝑑) 𝑡1 = 𝑟𝑒𝑑𝑢𝑐𝑒(𝑓𝑚(𝑥, 𝑦) = if 𝑦 > 𝑥 then 𝑦 else 𝑥, 𝑑)
JTR - Substituição de Transformação de Junção (Join Transformation Re- placement): para cada transformação de junção (innerJoin, leftOuterJoin, rightOuter-
Join e fullOuterJoin) existente no programa, este operador de mutação gera três mutantes ao substituir a transformação pelas outras três transformações de junção restantes. Além disso, também é inserida uma transformação de mapeamento após a junção para ajustar o tipo do conjunto de dados resultante da nova transformação de junção com o tipo da junção antiga. Isso é necessário porque os diferentes tipos de junção possuem diferenças em relação à presença de algum valor do lado esquerdo, do lado direito ou de ambos os lados na junção, fazendo com que alguns desses possam ser opcionais (no modelo repre- sentamos esse valor opcional como uma bag que pode ser vazia ou conter um elemento). Por exemplo, a substituição de uma transformação innerJoin por uma transformação lef- tOuterJoin faz com que o valor do lado direito da junção se torne opcional, sendo que na transformação original esse valor era obrigatório. Por esse motivo, precisamos inserir uma transformação de mapeamento após a nova transformação de junção para ajustar os valores opcionais com a junção antiga e manter a consistência de tipos do programa. Quando o novo valor que precisa ser mapeado para o tipo antigo for vazio, é necessário atribuir algum valor para que a consistência de tipos seja mantida. Assim, propomos substituir esse valor vazio por um valor padrão, caso o tipo do valor seja um tipo básico, ou por null, em caso contrário.
Para exemplificar, vamos considerar que no programa existe a transformação 𝑡1 = 𝑖𝑛𝑛𝑒𝑟𝐽 𝑜𝑖𝑛(𝑑1, 𝑑2), então a aplicação do operador de mutação irá gerar os seguintes
mutantes:
𝑡1 = 𝑙𝑒𝑓 𝑡𝑂𝑢𝑡𝑒𝑟𝐽 𝑜𝑖𝑛(𝑑1, 𝑑2)
𝑡𝑚 = 𝑚𝑎𝑝(𝜆(𝑘, (𝑥, 𝑦)).if 𝑦 = {{}} then (𝑘, (𝑥, 𝑣𝑑)) else (𝑘, (𝑥, 𝑣𝑦)), 𝑑𝑚)
𝑡1 = 𝑟𝑖𝑔ℎ𝑡𝑂𝑢𝑡𝑒𝑟𝐽 𝑜𝑖𝑛(𝑑1, 𝑑2)
𝑡𝑚 = 𝑚𝑎𝑝(𝜆(𝑘, (𝑥, 𝑦)).if 𝑥 = {{}} then (𝑘, (𝑣𝑑, 𝑦)) else (𝑘, (𝑣𝑥, 𝑦)), 𝑑𝑚)
𝑡1 = 𝑓 𝑢𝑙𝑙𝑂𝑢𝑡𝑒𝑟𝐽 𝑜𝑖𝑛(𝑑1, 𝑑2)
𝑡𝑚 = 𝑚𝑎𝑝(𝜆(𝑘, (𝑥, 𝑦)).if 𝑥 ̸= {{}} ∧ 𝑦 = {{}} then (𝑘, (𝑣𝑥, 𝑣𝑑)) else 𝑓𝑚(𝑘, (𝑥, 𝑦)), 𝑑𝑚) 𝑓𝑚(𝑘, (𝑥, 𝑦)) = if 𝑥 = {{}} ∧ 𝑦 ̸= {{}} then (𝑘, (𝑣𝑑, 𝑣𝑦)) else (𝑘, (𝑣𝑥, 𝑣𝑦))
Nos mutantes acima, a transformação 𝑡𝑚 representa a transformação de mapea- mento que foi inserida após a nova transformação de junção 𝑡1 e 𝑑𝑚 representa o conjunto de dados resultante de 𝑡1. Os valores 𝑣𝑥 e 𝑣𝑦 representam os valores de 𝑥 e 𝑦 quando esses forem opcionais e não forem vazios (𝑥 = {{𝑣𝑥}} e 𝑦 = {{𝑣𝑦}}). O valor 𝑣𝑑 representa um valor padrão que deve ser inserido quando um valor opcional for vazio. As transformações que recebiam o conjunto de dados resultante de 𝑡1 como entrada também são ajustadas para receber o conjunto de dados resultante de 𝑡𝑚.
OTD - Remoção de Transformação de Ordenação (Order Transformation Deletion): para cada transformação de ordenação (orderBy e orderByKey) no pro-
grama, este operador de mutação gera um mutante em que a transformação de orde- nação é substituída pela transformação identidade do conjunto de dados de entrada da transformação original. Esse operador é uma especialização do operador UTD. Para exemplificar, vamos considerar que no programa existe a transformação de ordenação 𝑡1 = 𝑜𝑟𝑑𝑒𝑟𝐵𝑦𝐾𝑒𝑦(𝑡𝑟𝑢𝑒, 𝑑) que ordena os valores a partir das chaves de forma descen- dente, a aplicação de OTD geraria o seguinte mutante:
𝑡1 = 𝑖𝑑𝑒𝑛𝑡𝑖𝑡𝑦(𝑑)
OTI - Inversão de Transformação de Ordenação (Order Transformation Invertion): para cada transformação de ordenação (orderBy e orderByKey) existente
no programa, este operador de mutação gera um mutante que inverte a ordem (ascendente ou descendente) da transformação original. Por exemplo, considerando o mesmo exemplo utilizado para ilustrar o operador OTD, a aplicação do operador OTI vai gerar um mutante em que o conjunto de dados é ordenado de forma ascendente:
Tabela 6.3 – Mutantes gerados com os operadores de mutação de fluxo de dados.
Id Operador Linhas Mutação
1 UTR 1 val even =input.map( x =>x * x )
2 UTR 1 val even =input.sortBy( x =>x )
3 UTR 2 val square =even.filter( x =>x % 2 ==0 )
4 UTR 2 val square =even.sortBy( x =>x )
5 UTR 3 val sorted =square.filter( x =>x % 2 ==0 )
6 UTR 3 val sorted =square.map( x =>x * x )
7 UTS 1,2 val even = input.map(x => x * x)
val square = even.filter(x => x % 2 == 0)
8 UTS 1,3 val even = input.sortBy(x => x)
val sorted = square.filter(x => x % 2 == 0)
9 UTS 2,3 val square = even.sortBy(x => x)
val sorted = square.map(x => x * x)
10 UTD 1 val even =input
11 UTD 2 val square =even
12 UTD 3 val sorted =square