Para obter um código de produção de alta qualidade, não basta garantir a máxima cobertura com os testes. Sem dúvida, ótimos resultados exigem que o código principal do projeto e os testes funcionem eficientemente juntos. Portanto, os testes devem receber tanta atenção quanto o código principal. Um teste decente é um fator chave para o sucesso, uma vez que sofrerá regressão na produção. Vamos dar uma olhada nos avisos do analisador estático do PVS-Studio para ver a importância do fato de que os erros nos testes não são piores que os da produção. Foco de hoje: Apache Hadoop.
Sobre o projeto
Aqueles que anteriormente estavam interessados em Big Data provavelmente já ouviram ou até trabalharam com o projeto
Apache Hadoop . Em poucas palavras, o Hadoop é uma estrutura que pode ser usada como base para criar sistemas de Big Data e trabalhar com eles.
O Hadoop consiste em quatro módulos principais, cada um deles executa uma tarefa específica necessária para um sistema de análise de big data:
- Hadoop comum
- Mapreduce
- Sistema de arquivos distribuídos do Hadoop
- Fios
De qualquer forma, há muitos materiais sobre isso na Internet.
Sobre a verificação
Conforme mostrado na documentação, o PVS-Studio pode ser integrado ao projeto de várias maneiras:
- Usando o plugin maven;
- Usando o plugin gradle;
- Usando o gradle IntellJ IDEA;
- Diretamente usando o analisador.
O Hadoop é baseado no sistema de criação de maven, portanto, não houve obstáculos na verificação.
Depois de integrar o script da documentação e editar um dos arquivos pom.xml (havia módulos nas dependências que não estavam disponíveis), a análise começou!
Após a conclusão da análise, escolhi os avisos mais interessantes e notei que tinha o mesmo número de avisos no código de produção e nos testes. Normalmente, não considero avisos do analisador, dados para testes. Mas quando os dividi, não pude deixar as advertências de 'testes' desacompanhadas. “Por que não dar uma olhada neles?” - pensei, porque erros nos testes também podem ter consequências adversas. Eles podem levar a testes incorretos ou parciais, ou até mesmo a erros (eles existem apenas para marcar a caixa, se estão sempre verdes).
Depois de selecionar os avisos mais intrigantes, os dividi pelos seguintes grupos: produção, teste e os quatro principais módulos do Hadoop. E agora, fico feliz em oferecer a sua atenção a revisão dos avisos do analisador.
Código de produção
Hadoop comum
V6033 Um item com a mesma chave 'KDC_BIND_ADDRESS' já foi adicionado. MiniKdc.java (163), MiniKdc.java (162)
public class MiniKdc { .... private static final Set<String> PROPERTIES = new HashSet<String>(); .... static { PROPERTIES.add(ORG_NAME); PROPERTIES.add(ORG_DOMAIN); PROPERTIES.add(KDC_BIND_ADDRESS); PROPERTIES.add(KDC_BIND_ADDRESS);
O valor adicionado duas vezes no
HashSet é um defeito muito comum na verificação de projetos. A segunda adição será realmente ignorada. Espero que essa duplicação seja apenas uma tragédia desnecessária. E se outro valor fosse criado para ser adicionado?
Mapreduce
V6072 Dois fragmentos de código semelhantes foram encontrados. Talvez seja um erro de digitação e a variável 'localFiles' deva ser usada em vez de 'localArchives'. LocalDistributedCacheManager.java (183), LocalDistributedCacheManager.java (178), LocalDistributedCacheManager.java (176), LocalDistributedCacheManager.java (181)
public synchronized void setup(JobConf conf, JobID jobId) throws IOException { ....
O diagnóstico V6072 às vezes produz algumas descobertas interessantes. O objetivo deste diagnóstico é detectar os mesmos fragmentos de código de tipo resultantes da cópia-pasta e substituição de uma-duas variáveis. Nesse caso, algumas variáveis foram deixadas "não alteradas".
O código acima demonstra isso. No primeiro bloco, a variável
localArchives é usada, no segundo fragmento semelhante -
localFiles . Se você estudar esse código com a devida diligência, em vez de executá-lo rapidamente, como geralmente acontece durante a revisão do código, você notará o fragmento, onde o autor esqueceu de substituir a variável
localArchives .
Essa gafe pode levar ao seguinte cenário:
- Suponha que tenhamos arquivos locais (tamanho = 4) e arquivos locais (tamanho = 2);
- Ao criar a matriz localFiles.toArray (nova String [localArchives.size ()]) , os dois últimos elementos serão nulos (["pathToFile1", "pathToFile2", null, null]);
- Então org.apache.hadoop.util.StringUtils.arrayToString retornará a representação de string da nossa matriz, na qual os nomes dos últimos arquivos serão apresentados como "nulos" ("pathToFile1, pathToFile2, null, null" ) ;
- Tudo isso será passado adiante e Deus sabe apenas que tipo de verificação existem para esses casos =).
A expressão
V6007 'children.size ()> 0' sempre é verdadeira. Queue.java (347)
boolean isHierarchySameAs(Queue newState) { .... if (children == null || children.size() == 0) { .... } else if(children.size() > 0) { .... } .... }
Devido ao fato de o número de elementos ser verificado como 0 separadamente, a verificação adicional
children.size ()> 0 sempre será verdadeira.
HDFS
V6001 Existem
subexpressões idênticas 'this.bucketSize' à esquerda e à direita do operador '%'. RollingWindow.java (79)
RollingWindow(int windowLenMs, int numBuckets) { buckets = new Bucket[numBuckets]; for (int i = 0; i < numBuckets; i++) { buckets[i] = new Bucket(); } this.windowLenMs = windowLenMs; this.bucketSize = windowLenMs / numBuckets; if (this.bucketSize % bucketSize != 0) {
Aqui o defeito é que a variável é dividida por ela mesma. Como resultado, a verificação da multiplicidade sempre será bem-sucedida e, no caso de obter entradas incorretas (
windowLenMs ,
numBuckets ), a exceção nunca será lançada.
Fios
V6067 Dois ou mais ramificações de caso executam as mesmas ações. TimelineEntityV2Converter.java (386), TimelineEntityV2Converter.java (389)
public static ApplicationReport convertToApplicationReport(TimelineEntity entity) { .... if (metrics != null) { long vcoreSeconds = 0; long memorySeconds = 0; long preemptedVcoreSeconds = 0; long preemptedMemorySeconds = 0; for (TimelineMetric metric : metrics) { switch (metric.getId()) { case ApplicationMetricsConstants.APP_CPU_METRICS: vcoreSeconds = getAverageValue(metric.getValues().values()); break; case ApplicationMetricsConstants.APP_MEM_METRICS: memorySeconds = ....; break; case ApplicationMetricsConstants.APP_MEM_PREEMPT_METRICS: preemptedVcoreSeconds = ....;
Os mesmos fragmentos de código em dois ramos do
caso . Está em todo lugar! No número prevalecente de casos, esse não é um erro real, mas apenas um motivo para pensar sobre o fato de
alterar a opção . Mas não para o caso em questão. Fragmentos de código repetidos configuram o valor da variável
preemptedVcoreSeconds . Se você observar atentamente os nomes de todas as variáveis e constantes, provavelmente concluirá que, se
metric.getId () == APP_MEM_PREEMPT_METRICS, o valor deve ser definido para a variável
preemptedMemorySeconds , não para
preemptedVcoreSeconds . Nesse sentido, após o operador 'switch',
preemptedMemorySeconds sempre permanecerá 0, enquanto o valor de
preemptedVcoreSeconds pode estar incorreto.
V6046 Formato incorreto. É esperado um número diferente de itens de formato. Argumentos não utilizados: 2. AbstractSchedulerPlanFollower.java (186)
@Override public synchronized void synchronizePlan(Plan plan, boolean shouldReplan) { .... try { setQueueEntitlement(planQueueName, ....); } catch (YarnException e) { LOG.warn("Exception while trying to size reservation for plan: {}", currResId, planQueueName, e); } .... }
A variável
planQueueName não é usada durante o log. Nesse caso, muita coisa é copiada ou a sequência de formatação não está concluída. Mas ainda culpo o bom e velho copiar-colar, que em alguns casos é ótimo para dar um tiro no pé.
Código de teste
Hadoop comum
V6072 Dois fragmentos de código semelhantes foram encontrados. Talvez seja um erro de digitação e a variável 'allSecretsB' deva ser usada em vez de 'allSecretsA'. TestZKSignerSecretProvider.java (316), TestZKSignerSecretProvider.java (309), TestZKSignerSecretProvider.java (306), TestZKSignerSecretProvider.java (313)
public void testMultiple(int order) throws Exception { .... currentSecretA = secretProviderA.getCurrentSecret(); allSecretsA = secretProviderA.getAllSecrets(); Assert.assertArrayEquals(secretA2, currentSecretA); Assert.assertEquals(2, allSecretsA.length);
E novamente o V6072. Observe atentamente as variáveis
allSecretsA e
allSecretsB .
V6043 Considere inspecionar o operador 'for'. Os valores inicial e final do iterador são os mesmos. TestTFile.java (235)
private int readPrepWithUnknownLength(Scanner scanner, int start, int n) throws IOException { for (int i = start; i < start; i++) { String key = String.format(localFormatter, i); byte[] read = readKey(scanner); assertTrue("keys not equal", Arrays.equals(key.getBytes(), read)); try { read = readValue(scanner); assertTrue(false); } catch (IOException ie) {
Um teste sempre verde? =). Uma parte do loop, que faz parte do próprio teste, nunca será executada. Isso se deve ao fato de que os valores inicial e final do contador são iguais na instrução
for . Como resultado, a condição
i <start se tornará imediatamente falsa, levando a esse comportamento. Corri o arquivo de teste e cheguei à conclusão de que, na verdade,
eu <(start + n) tinha que ser escrito na condição de loop.
Mapreduce
A expressão
V6007 'byteAm <0' é sempre falsa. DataWriter.java (322)
GenerateOutput writeSegment(long byteAm, OutputStream out) throws IOException { long headerLen = getHeaderLength(); if (byteAm < headerLen) {
A condição
byteAm <0 é sempre falsa. Para descobrir, vamos dar uma olhada no código acima. Se a execução do teste atingir a operação
byteAm - = headerLen , significa que
byteAm> = headerLen. A partir daqui, após a subtração, o valor
byteAm nunca será negativo. Isso é o que tivemos que provar.
HDFS
V6072 Dois fragmentos de código semelhantes foram encontrados. Talvez seja um erro de digitação e a variável 'normalFile' deva ser usada em vez de 'normalDir'. TestWebHDFS.java (625), TestWebHDFS.java (615), TestWebHDFS.java (614), TestWebHDFS.java (624)
public void testWebHdfsErasureCodingFiles() throws Exception { .... final Path normalDir = new Path("/dir"); dfs.mkdirs(normalDir); final Path normalFile = new Path(normalDir, "file.log"); ....
Acredite ou não, é V6072 novamente! Basta seguir as variáveis
normalDir e
normalFile.V6027 As variáveis são inicializadas através da chamada para a mesma função. Provavelmente é um erro ou código não otimizado. TestDFSAdmin.java (883), TestDFSAdmin.java (879)
private void verifyNodesAndCorruptBlocks( final int numDn, final int numLiveDn, final int numCorruptBlocks, final int numCorruptECBlockGroups, final DFSClient client, final Long highestPriorityLowRedundancyReplicatedBlocks, final Long highestPriorityLowRedundancyECBlocks) throws IOException { .... final String expectedCorruptedECBlockGroupsStr = String.format( "Block groups with corrupt internal blocks: %d", numCorruptECBlockGroups); final String highestPriorityLowRedundancyReplicatedBlocksStr = String.format( "\tLow redundancy blocks with highest priority " + "to recover: %d", highestPriorityLowRedundancyReplicatedBlocks); final String highestPriorityLowRedundancyECBlocksStr = String.format( "\tLow redundancy blocks with highest priority " + "to recover: %d", highestPriorityLowRedundancyReplicatedBlocks); .... }
Nesse fragmento, as variáveis
maximumPriorityLowRedundancyReplicatedBlocksStr e
maximumPriorityLowRedundancyECBlocksStr são inicializadas pelos mesmos valores. Muitas vezes deve ser assim, mas este não é o caso. Os nomes das variáveis são longos e similares, portanto, não é de surpreender que o fragmento copiado e colado não tenha sido alterado de forma alguma. Para corrigi-lo, ao inicializar a variável
maximumPriorityLowRedundancyECBlocksStr , o autor deve usar o parâmetro de entrada
maximumPriorityLowRedundancyECBlocks . Além disso, provavelmente, eles ainda precisam corrigir a linha de formato.
V6019 Código inacessível detectado. É possível que haja um erro. TestReplaceDatanodeFailureReplication.java (222)
private void verifyFileContent(...., SlowWriter[] slowwriters) throws IOException { LOG.info("Verify the file"); for (int i = 0; i < slowwriters.length; i++) { LOG.info(slowwriters[i].filepath + ....); FSDataInputStream in = null; try { in = fs.open(slowwriters[i].filepath); for (int j = 0, x;; j++) { x = in.read(); if ((x) != -1) { Assert.assertEquals(j, x); } else { return; } } } finally { IOUtils.closeStream(in); } } }
O analisador reclama que o contador
i ++ no loop não pode ser alterado. O que significa que no loop
for (int i = 0; i <slowwriters.length; i ++) {....}, não mais do que uma iteração será executada. Vamos descobrir o porquê. Portanto, na primeira iteração, vinculamos o thread ao arquivo que corresponde a
slowwriters [0] para leitura adicional. Em seguida, lemos o conteúdo do arquivo através do loop
for (int j = 0, x ;; j ++):- se lemos algo relevante, comparamos o byte lido com o valor atual do contador j, embora assertEquals (se a verificação não for bem-sucedida, falhamos no teste);
- se o arquivo for verificado com êxito e chegarmos ao final do arquivo (lemos -1), o método será encerrado.
Portanto, o que quer que aconteça durante a verificação de
slowwriters [0] , não será possível verificar os elementos subsequentes. Provavelmente, é necessário usar o
intervalo em vez do
retorno.Fios
V6019 Código inacessível detectado. É possível que haja um erro. TestNodeManager.java (176)
@Test public void testCreationOfNodeLabelsProviderService() throws InterruptedException { try { .... } catch (Exception e) { Assert.fail("Exception caught"); e.printStackTrace(); } }
Nessa situação, o método
Assert.fail interromperá o teste e o rastreamento de pilha não será impresso em caso de exceção. Se a mensagem sobre a exceção capturada for suficiente aqui, é melhor remover a impressão do stacktrace para evitar confusão. Se for necessário imprimir, basta trocá-los.
Muitos fragmentos semelhantes foram encontrados:
- V6019 Código inacessível detectado. É possível que haja um erro. TestResourceTrackerService.java (928)
- V6019 Código inacessível detectado. É possível que haja um erro. TestResourceTrackerService.java (737)
- V6019 Código inacessível detectado. É possível que haja um erro. TestResourceTrackerService.java (685)
- ....
V6072 Dois fragmentos de código semelhantes foram encontrados. Talvez seja um erro de digitação e a variável 'publicCache' deva ser usada em vez de 'usercache'. TestResourceLocalizationService.java (315), TestResourceLocalizationService.java (309), TestResourceLocalizationService.java (307), TestResourceLocalizationService.java (317)
@Test public void testDirectoryCleanupOnNewlyCreatedStateStore() throws IOException, URISyntaxException { ....
E, finalmente, V6072 novamente =). Variáveis para visualizar o fragmento suspeito:
usercache e
publicCache .
Conclusão
Centenas de milhares de linhas de código são escritas em desenvolvimento. O código de produção geralmente é mantido livre de bugs, defeitos e falhas (os desenvolvedores testam seu código, o código é revisado e assim por diante). Os testes são definitivamente inferiores a esse respeito. Defeitos nos testes podem se esconder facilmente atrás de um "sinal verde". Como você provavelmente recebeu da revisão de avisos de hoje, um teste verde nem sempre é uma verificação bem-sucedida.
Dessa vez, ao verificar a base de código do Apache Hadoop, a análise estática acabou sendo altamente necessária no código de produção e nos testes que também desempenham um papel importante no desenvolvimento.
Portanto, se você se importa com o seu código e testa a qualidade, sugiro que você analise a estática. Deixe o
PVS-Studio ser o primeiro candidato neste empreendimento.