Qualidade de código do Apache Hadoop: teste VS de produção

Figura 1

Para obter código de produção de alta qualidade, não basta fornecer a máxima cobertura de teste. Sem dúvida, para alcançar altos resultados, o código e os testes principais do projeto devem funcionar em um conjunto perfeitamente coeso. Portanto, você precisa prestar atenção nos testes, tanto quanto no código principal. Escrever um bom teste é a chave para obter uma regressão na produção. Para mostrar a importância do fato de que os erros nos testes não são piores que na produção, consideraremos a próxima análise de avisos do analisador estático PVS-Studio. Alvo: Apache Hadoop.

Sobre o projeto


Aqueles que já se interessaram por Big Data provavelmente já ouviram ou trabalharam em um projeto como o Apache Hadoop . Em resumo, o Hadoop é uma estrutura que pode ser usada como base para criar e trabalhar com sistemas de Big Data.

O Hadoop consiste em quatro módulos principais, cada um dos quais 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 (Sistema de arquivos distribuídos do Hadoop)
  • Fios

No entanto, existem muitos materiais para se familiarizar com ele 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 IntellJ IDEA
  • usando o analisador diretamente.

O Hadoop foi construído com base no sistema de compilação maven, portanto não houve dificuldades com a verificação.

Tendo integrado o script da documentação e ajustado um pouco do pom.xml (havia módulos nas dependências que não estavam lá), a análise foi feita!

Após a análise, escolhendo os avisos mais interessantes, notei que tinha o mesmo número de avisos no código de produção e nos testes. Normalmente, não considero o acionador do analisador que cai nos testes. Mas, dividindo-os, não pude perder os avisos da categoria 'testes' que passaram pela minha atenção. "Por que não?", Pensei, porque os erros nos testes também têm consequências. Eles podem levar a testes incorretos ou parciais, ou até a bobagens (apenas para exibição, para que sejam sempre verdes).

Portanto, depois de coletar os avisos mais interessantes, dividindo-os por código (produção, teste) e os quatro módulos principais do Hadoop, trago à sua atenção uma análise das operações 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); // <= PROPERTIES.add(KDC_PORT); PROPERTIES.add(INSTANCE); .... } .... } 

Um valor adicionado duas vezes a um HashSet é um defeito comum na verificação de projetos. De fato, a segunda adição será ignorada. Bem, se essa duplicação for um acidente absurdo. Mas e se realmente significasse adicionar outro valor?

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 { .... // Update the configuration object with localized data. if (!localArchives.isEmpty()) { conf.set(MRJobConfig.CACHE_LOCALARCHIVES, StringUtils .arrayToString(localArchives.toArray(new String[localArchives // <= .size()]))); } if (!localFiles.isEmpty()) { conf.set(MRJobConfig.CACHE_LOCALFILES, StringUtils .arrayToString(localFiles.toArray(new String[localArchives // <= .size()]))); } .... } 

O Diagnostics V6072 faz algumas vezes descobertas muito interessantes. A essência do diagnóstico é procurar o mesmo tipo de fragmento de código obtido por copiar-colar e substituir uma ou duas variáveis, mas ao mesmo tempo algumas variáveis ​​foram "subestimadas".

O código acima demonstra isso. No primeiro bloco, as ações são executadas com a variável localArchives , no próximo bloco do mesmo tipo, com localFiles . E se você estuda conscientemente esse código e não o repassa rapidamente, como costuma ser o caso com a revisão de código, observe o local em que você esqueceu de substituir a variável localArchives .

Essa supervisão 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 ()]) , obtemos os dois últimos elementos nulos (["pathToFile1", "pathToFile2", null, null]);
  • Depois disso, org.apache.hadoop.util.StringUtils.arrayToString retornará uma representação de string de nossa matriz na qual os últimos nomes de arquivos serão representados como "nulos" ("pathToFile1, pathToFile2, null, null" ) ;
  • Tudo isso será repassado e quem sabe quais verificações 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 que a verificação do número de elementos em 0 é feita separadamente, a verificação adicional de 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) { // <= throw new IllegalArgumentException( "The bucket size in the rolling window is not integer: windowLenMs= " + windowLenMs + " numBuckets= " + numBuckets); } } 

Esse defeito reside no fato de que a variável é dividida em si mesma. Como resultado, a verificação da multiplicidade sempre será aprovada e, no caso de dados de entrada incorretos ( windowLenMs , numBuckets ), a exceção não 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 = ....; // <= break; case ApplicationMetricsConstants.APP_CPU_PREEMPT_METRICS: preemptedVcoreSeconds = ....; // <= break; default: // Should not happen.. break; } } .... } .... } 

Nos dois ramos do caso , o mesmo código fragmenta. Isso acontece o tempo todo! No número predominante de casos, esse não é um erro real, mas apenas uma ocasião para pensar em refatoração de comutador . Mas não para o caso em questão. Na repetição de trechos de código, o valor da variável preemptedVcoreSeconds é definido . Se você prestar atenção aos nomes de todas as variáveis ​​e constantes, poderá concluir que, no caso de metric.getId () == APP_MEM_PREEMPT_METRICS , o valor da variável preemptedMemorySeconds deve ser definido, não preemptedVcoreSeconds . Nesse sentido, preemptedMemorySeconds sempre permanecerá 0 após a execução da instrução 'switch', e 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); } .... } 

Variável não utilizada planQueueName durante o log. Aqui, eles copiaram demais ou não modificaram a string de formato. Mas, no entanto, sou inclinado ao bom e velho serviço de copiar e colar, às vezes baixo.

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); // <= Assert.assertArrayEquals(secretA2, allSecretsA[0]); Assert.assertArrayEquals(secretA1, allSecretsA[1]); currentSecretB = secretProviderB.getCurrentSecret(); allSecretsB = secretProviderB.getAllSecrets(); Assert.assertArrayEquals(secretA2, currentSecretB); Assert.assertEquals(2, allSecretsA.length); // <= Assert.assertArrayEquals(secretA2, allSecretsB[0]); Assert.assertArrayEquals(secretA1, allSecretsB[1]); .... } 

E novamente V6072. Cuidado com 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) { // should have thrown exception } String value = "value" + key; read = readLongValue(scanner, value.getBytes().length); assertTrue("values nto equal", Arrays.equals(read, value.getBytes())); scanner.advance(); } return (start + n); } 

Um teste sempre verde? =). O corpo do loop, que faz parte do teste, nunca é executado. Isso se deve ao fato de os valores inicial e final do contador corresponderem na instrução for . Como resultado, a condição i <start imediatamente nos fornecerá false, o que levará a esse comportamento. Corri o arquivo com os testes e cheguei à conclusão de que era necessário escrever na condição do loop i <(start + n) .

Mapreduce


a href = " www.viva64.com/en/w/v6007 "> 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) { // not enough bytes to write even the header return new GenerateOutput(0, 0); } // adjust for header length byteAm -= headerLen; if (byteAm < 0) { // <= byteAm = 0; } .... } 

A condição byteAm <0 é sempre falsa. Para entender, vamos subir o código acima. Se a execução do teste atingir a operação byteAm - = headerLen , isso significa que haverá byteAm> = headerLen . A partir daqui, após executar a subtração, o valor de byteAm nunca será negativo. O que era necessário para 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"); .... // logic block #1 FileStatus expectedNormalDirStatus = dfs.getFileStatus(normalDir); FileStatus actualNormalDirStatus = webHdfs.getFileStatus(normalDir); // <= Assert.assertEquals(expectedNormalDirStatus.isErasureCoded(), actualNormalDirStatus.isErasureCoded()); ContractTestUtils.assertNotErasureCoded(dfs, normalDir); assertTrue(normalDir + " should have erasure coding unset in " + ....); // logic block #2 FileStatus expectedNormalFileStatus = dfs.getFileStatus(normalFile); FileStatus actualNormalFileStatus = webHdfs.getFileStatus(normalDir); // <= Assert.assertEquals(expectedNormalFileStatus.isErasureCoded(), actualNormalFileStatus.isErasureCoded()); ContractTestUtils.assertNotErasureCoded(dfs, normalFile); assertTrue( normalFile + " should have erasure coding unset in " + ....); } 

Não acredito nisso, e novamente V6072! 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 { /* init vars */ .... 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 com os mesmos valores. Freqüentemente deveria ser, mas não nessa situação. Os nomes das variáveis ​​aqui são longos e semelhantes entre si, portanto, não estou surpreso por não haver modificações correspondentes com copiar e colar. Para solucionar a situação, ao inicializar a variável maximumPriorityLowRedundancyECBlocksStr, você deve usar o parâmetro de entrada maximumPriorityLowRedundancyECBlocks . Além disso, provavelmente, você ainda precisa corrigir a string 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 jura que a alteração do contador i ++ no loop é inatingível. Isso significa que em um loop for (int i = 0; i <slowwriters.length; i ++) {....}, não será executada mais de uma iteração. Vamos descobrir o porquê. Portanto, na primeira iteração, associamos o fluxo ao arquivo correspondente a slowwriters [0] para leitura adicional. Através do loop for (int j = 0, x ;; j ++), lemos o conteúdo do arquivo por byte, onde:

  • se lemos algo adequado, por meio de assertEquals, comparamos o byte de leitura com o valor atual do contador j (no caso de verificação sem êxito, encerramos o teste com falha);
  • se o arquivo passou no teste e chegamos ao final do arquivo (leia -1), sairemos do método.

Portanto, não importa o que aconteça ao verificar slowwriters [0] , ele não chegará à verificação dos seguintes elementos. Provavelmente, deve-se 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 stacktrace nunca será impresso se ocorrer uma exceção, porque o método Assert.fail interromperá o teste. Se houver uma mensagem suficiente de que a exceção foi detectada, para não confundir, a impressão de rastreamento de pilha precisa ser removida. Se for necessário imprimir, basta trocá-los.

Existem muitos lugares assim:
  • 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 { .... // verify directory creation for (Path p : localDirs) { p = new Path((new URI(p.toString())).getPath()); // logic block #1 Path usercache = new Path(p, ContainerLocalizer.USERCACHE); verify(spylfs).rename(eq(usercache), any(Path.class), any()); // <= verify(spylfs).mkdir(eq(usercache), ....); // logic block #2 Path publicCache = new Path(p, ContainerLocalizer.FILECACHE); verify(spylfs).rename(eq(usercache), any(Path.class), any()); // <= verify(spylfs).mkdir(eq(publicCache), ....); .... } .... } 

E, finalmente, novamente V6072 =). Variáveis ​​para se familiarizar com o snippet suspeito: usercache e publicCache .

Conclusão


Durante o desenvolvimento, centenas de milhares de linhas de código são escritas. Se o código de produção está tentando se manter limpo de bugs, defeitos e deficiências (o desenvolvedor testa seu próprio código, realiza uma revisão de código e muito mais), os testes são claramente inferiores a isso. Defeitos nos testes podem se esconder silenciosamente atrás de um "sinal verde". E como você entende pela análise atual dos avisos, um teste aprovado com sucesso está longe de ser sempre um teste garantido.

Ao verificar a base de código do Apache Hadoop, a análise estática demonstrou sua necessidade não apenas do código que entra em produção, mas também de testes que também desempenham um papel importante no desenvolvimento.

Portanto, se você se preocupa com a qualidade do seu código e da base de testes, recomendo que analise a estática. E o primeiro candidato ao teste, proponho experimentar o PVS-Studio .



Se você deseja compartilhar este artigo com um público que fala inglês, use o link para a tradução: Maxim Stefanov. Qualidade do código do Apache Hadoop: teste de produção vs.

Source: https://habr.com/ru/post/pt480928/


All Articles