Pour obtenir un code de production de haute qualité, il ne suffit pas de fournir une couverture de test maximale. Sans aucun doute, pour obtenir des résultats élevés, le code principal du projet et les tests doivent fonctionner en tandem parfaitement cohérent. Par conséquent, vous devez faire attention aux tests autant qu'au code principal. La rédaction d'un bon test est la clé pour rattraper une régression de la production. Pour montrer l'importance du fait que les bogues dans les tests ne sont pas pires qu'en production, nous considérerons la prochaine analyse des avertissements de l'analyseur statique PVS-Studio. Cible: Apache Hadoop.
À propos du projet
Ceux qui étaient autrefois intéressés par le Big Data ont probablement entendu parler ou travaillé avec un projet comme
Apache Hadoop . En bref, Hadoop est un framework qui peut être utilisé comme base pour construire et travailler avec des systèmes Big Data.
Hadoop se compose de quatre modules principaux, chacun effectuant une tâche spécifique nécessaire à un système d'analyse de Big Data:
- Hadoop commun
- Mapreduce
- Système de fichiers distribué Hadoop (Système de fichiers distribué Hadoop)
- Fil
Cependant, il existe de nombreux documents pour vous familiariser avec Internet.
À propos de la vérification
Comme indiqué dans la
documentation , PVS-Studio peut être intégré au projet de différentes manières:
- en utilisant le plugin maven;
- en utilisant le plugin gradle;
- Utilisation d'IntellJ IDEA
- en utilisant l'analyseur directement.
Hadoop a été construit sur la base du système de construction maven, il n'y a donc eu aucune difficulté avec la vérification.
Après avoir intégré le script de la documentation et légèrement ajusté un de pom.xml (il y avait des modules dans les dépendances qui n'étaient pas là), l'analyse est allée!
Après l'analyse, en choisissant les avertissements les plus intéressants, j'ai remarqué que j'avais le même nombre d'avertissements dans le code de production et les tests. Habituellement, je ne considère pas le déclenchement d'un analyseur qui tombe sur des tests. Mais, en les divisant, je ne pouvais pas manquer les avertissements de la catégorie «tests» devant mon attention. «Pourquoi pas?», Ai-je pensé, car les bogues dans les tests ont également des conséquences. Ils peuvent conduire à des tests incorrects ou partiels, voire à des bêtises (juste pour le spectacle, afin qu'ils soient toujours verts).
Ainsi, en rassemblant les avertissements les plus intéressants, en les divisant par code (production, test) et les quatre principaux modules Hadoop, je porte à votre attention une analyse du fonctionnement de l'analyseur.
Code de production
Hadoop commun
V6033 Un élément avec la même clé 'KDC_BIND_ADDRESS' a déjà été ajouté. 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);
Une valeur ajoutée deux fois à un
HashSet est un défaut courant lors de la vérification de projets. En fait, le deuxième ajout sera ignoré. Eh bien, si cette duplication est un accident absurde. Et si cela signifiait vraiment ajouter une autre valeur?
Mapreduce
V6072 Deux fragments de code similaires ont été trouvés. Il s'agit peut-être d'une faute de frappe et la variable «localFiles» devrait être utilisée à la place de «localArchives». LocalDistributedCacheManager.java (183), LocalDistributedCacheManager.java (178), LocalDistributedCacheManager.java (176), LocalDistributedCacheManager.java (181)
public synchronized void setup(JobConf conf, JobID jobId) throws IOException { ....
Diagnostics V6072 fait des résultats parfois très intéressants. L'essence du diagnostic est de rechercher le même type de fragments de code qui ont été obtenus par copier-coller et en remplaçant une ou deux variables, mais en même temps, certaines variables étaient «sous-estimées».
Le code ci-dessus le démontre. Dans le premier bloc, les actions sont effectuées avec la variable
localArchives , dans le bloc suivant du même type, avec
localFiles . Et si vous étudiez consciencieusement ce code et ne le passez pas en revue rapidement, comme c'est souvent le cas avec la révision de code, notez l'endroit où vous avez oublié de remplacer la variable
localArchives .
Une telle omission pourrait conduire au scénario suivant:
- Supposons que nous ayons localArchives (taille = 4) et localFiles (taille = 2);
- Lors de la création du tableau localFiles.toArray (nouveau String [localArchives.size ()]) , nous obtenons que les 2 derniers éléments soient nuls (["pathToFile1", "pathToFile2", null, null]);
- Après cela, org.apache.hadoop.util.StringUtils.arrayToString renverra une représentation sous forme de chaîne de notre tableau dans laquelle les derniers noms de fichiers seront représentés comme "null" ("pathToFile1, pathToFile2, null, null" ) ;
- Tout cela sera transmis, et qui sait quels contrôles il y a pour de tels cas =).
V6007 L' expression 'children.size ()> 0' est toujours vraie. Queue.java (347)
boolean isHierarchySameAs(Queue newState) { .... if (children == null || children.size() == 0) { .... } else if(children.size() > 0) { .... } .... }
Étant donné que la vérification du nombre d'éléments à 0 est effectuée séparément, une vérification supplémentaire de
children.size ()> 0 donnera toujours la valeur true.
HDFS
V6001 Il existe des sous-expressions identiques 'this.bucketSize' à gauche et à droite de l'opérateur '%'. 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) {
Ce défaut réside dans le fait que la variable est divisée en elle-même. Par conséquent, la vérification de la multiplicité passera toujours et, en cas de données d'entrée incorrectes (
windowLenMs ,
numBuckets ), l'exception ne sera pas levée.
Fil
V6067 Deux ou plusieurs branches de cas effectuent les mêmes actions. 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 = ....;
Dans deux branches de
cas , les mêmes fragments de code. Cela arrive tout le temps! Dans le nombre prédominant de cas, ce n'est pas une vraie erreur, mais seulement une occasion de penser à une refactorisation des
commutateurs . Mais pas pour le cas en question. Dans les extraits de code répétitifs, la valeur de la variable
preemptedVcoreSeconds est définie . Si vous faites attention aux noms de toutes les variables et constantes, vous pouvez conclure que dans le cas de
metric.getId () == APP_MEM_PREEMPT_METRICS , la valeur de la variable
preemptedMemorySeconds doit être définie, et non
preemptedVcoreSeconds . À cet égard,
preemptedMemorySeconds restera toujours 0 après l'exécution de l'instruction 'switch', et la valeur de
preemptedVcoreSeconds peut être incorrecte.
V6046 Format incorrect. Un nombre différent d'éléments de format est attendu. Arguments non utilisés: 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); } .... }
Variable inutilisée
planQueueName lors de la connexion. Ici, ils ont trop copié ou n'ont pas modifié la chaîne de format. Mais néanmoins, je suis enclin au bon vieux et, en apportant parfois un mauvais service, au copier-coller.
Code de test
Hadoop commun
V6072 Deux fragments de code similaires ont été trouvés. Il s'agit peut-être d'une faute de frappe et la variable «allSecretsB» devrait être utilisée à la place 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);
Et encore V6072. Attention aux variables
allSecretsA et
allSecretsB .
V6043 Envisagez d'inspecter l'opérateur «pour». Les valeurs initiales et finales de l'itérateur sont identiques. 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) {
Un test toujours vert? =). Le corps de la boucle, qui fait partie du test, n'est jamais exécuté. Cela est dû au fait que les valeurs de début et de fin du compteur correspondent dans l'instruction
for . Par conséquent, la condition
i <start nous donnera immédiatement faux, ce qui conduira à ce comportement. J'ai parcouru le fichier avec les tests et suis arrivé à la conclusion qu'il était nécessaire d'écrire dans l'état de la boucle
i <(start + n) .
Mapreduce
a href = "
www.viva64.com/en/w/v6007 "> V6007 L'expression 'byteAm <0' est toujours fausse. DataWriter.java (322)
GenerateOutput writeSegment(long byteAm, OutputStream out) throws IOException { long headerLen = getHeaderLength(); if (byteAm < headerLen) {
La condition
byteAm <0 est toujours fausse. Pour comprendre, remontons le code ci-dessus. Si l'exécution du test atteint l'opération
byteAm - = headerLen , cela signifie qu'il y aura
byteAm> = headerLen . À partir de là, après avoir effectué la soustraction, la valeur de
byteAm ne sera jamais négative. Ce qui devait prouver.
HDFS
V6072 Deux fragments de code similaires ont été trouvés. Il s'agit peut-être d'une faute de frappe et la variable «normalFile» devrait être utilisée à la place 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"); ....
Ne le croyez pas, et encore V6072! Suivez simplement les
variables normalDir et
normalFileV6027 Les variables sont initialisées via l'appel à la même fonction. Il s'agit probablement d'une erreur ou d'un code non optimisé. 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); .... }
Dans ce fragment, les variables le plus
élevéPriorityLowRedundancyReplicatedBlocksStr et le plus
élevéPriorityLowRedundancyECBlocksStr sont initialisés avec les mêmes valeurs. C'est souvent le cas, mais pas dans cette situation. Les noms de variables ici sont longs et similaires les uns aux autres, donc je ne suis pas surpris qu'il n'y ait pas eu de modifications correspondantes avec le copier-coller. Pour remédier à la situation, lors de l'initialisation de la variable
LowestPriorityLowRedundancyECBlocksStr, vous devez utiliser le paramètre d'entrée
LowestPriorityLowRedundancyECBlocks . En plus de cela, très probablement, vous devez toujours corriger la chaîne de format.
V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. 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); } } }
L'analyseur jure que la modification du compteur
i ++ dans la boucle est impossible. Cela signifie que dans une boucle
for (int i = 0; i <slowwriters.length; i ++) {....} pas plus d'une itération ne sera effectuée. Voyons pourquoi. Ainsi, dans la première itération, nous associons le flux au fichier correspondant aux
ralentisseurs [0] pour une lecture plus approfondie. À travers la boucle
for (int j = 0, x ;; j ++), nous lisons le contenu du fichier par octet, où:
- si nous lisons quelque chose d'adéquat, alors via assertEquals nous comparons l'octet de lecture avec la valeur actuelle du compteur j (en cas de vérification infructueuse, nous quittons le test avec échec);
- si le fichier a réussi le test et que nous avons atteint la fin du fichier (lire -1), nous quittons la méthode.
Par conséquent, peu importe ce qui se passe lors de la vérification des
ralentisseurs [0] , il ne s'agit pas de vérifier les éléments suivants. Très probablement, la
pause devrait être utilisée au lieu du
retour .
Fil
V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. TestNodeManager.java (176)
@Test public void testCreationOfNodeLabelsProviderService() throws InterruptedException { try { .... } catch (Exception e) { Assert.fail("Exception caught"); e.printStackTrace(); } }
Dans cette situation, stacktrace ne sera jamais imprimé si une exception se produit, car la méthode
Assert.fail interrompra le test. S'il y a suffisamment de message que l'exception est interceptée, alors pour ne pas être confus, l'impression stacktrace'a doit être supprimée. Si l'impression est nécessaire, il vous suffit de les échanger.
Il existe de nombreux endroits de ce type:
- V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. TestResourceTrackerService.java (928)
- V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. TestResourceTrackerService.java (737)
- V6019 Code inaccessible détecté. Il est possible qu'une erreur soit présente. TestResourceTrackerService.java (685)
- ....
V6072 Deux fragments de code similaires ont été trouvés. Il s'agit peut-être d'une faute de frappe et la variable «publicCache» devrait être utilisée à la place de «usercache». TestResourceLocalizationService.java (315), TestResourceLocalizationService.java (309), TestResourceLocalizationService.java (307), TestResourceLocalizationService.java (313)
@Test public void testDirectoryCleanupOnNewlyCreatedStateStore() throws IOException, URISyntaxException { ....
Et enfin, encore V6072 =). Variables pour vous familiariser avec un fragment suspect:
usercache et
publicCache .
Conclusion
Pendant le développement, des centaines de milliers de lignes de code sont écrites. Si le code de production essaie de se maintenir à l'abri des bogues, des défauts et des lacunes (le développeur teste son propre code, procède à une révision du code, et bien plus encore), alors les tests sont clairement inférieurs à cela. Les défauts dans les tests peuvent se cacher discrètement derrière une "coche verte". Et comme vous le comprenez d'après l'analyse des avertissements d'aujourd'hui, un test réussi est loin d'être toujours un test garanti.
Lors de la vérification de la base de code Apache Hadoop, l'analyse statique a démontré son besoin non seulement pour le code qui entre en production, mais aussi pour les tests qui jouent également un rôle important dans le développement.
Donc, si vous vous souciez de la qualité de votre code et de votre base de test, je vous recommande de vous tourner vers l'analyse statique. Et le premier candidat au test, je propose d'essayer
PVS-Studio .

Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Maxim Stefanov.
Apache Hadoop Code Quality: Production VS Test .