Pour obtenir un code de production de haute qualité, il ne suffit pas d'assurer une couverture maximale avec les tests. Aucun doute, de bons résultats nécessitent que le code principal du projet et les tests fonctionnent efficacement ensemble. Par conséquent, les tests doivent recevoir autant d'attention que le code principal. Un test décent est un facteur clé de succès, car il entraînera une régression de la production. Jetons un coup d'œil aux avertissements de l'analyseur statique PVS-Studio pour voir l'importance du fait que les erreurs dans les tests ne sont pas pires que celles en production. Objectif d'aujourd'hui: Apache Hadoop.
À propos du projet
Ceux qui étaient auparavant intéressés par le Big Data ont probablement entendu parler ou même travaillé avec le projet
Apache Hadoop . En un mot, Hadoop est un framework qui peut être utilisé comme base pour construire des systèmes Big Data et travailler avec eux.
Hadoop se compose de quatre modules principaux, chacun d'eux effectue une tâche spécifique requise pour un système d'analyse de Big Data:
- Hadoop commun
- Mapreduce
- Système de fichiers distribué Hadoop
- Fil
Quoi qu'il en soit, il existe de nombreux documents à ce sujet sur Internet.
À propos du chèque
Comme indiqué dans la documentation, PVS-Studio peut être intégré au projet de différentes manières:
- Utilisation du plugin maven;
- Utilisation du plugin gradle;
- Utilisation du Gradle IntellJ IDEA;
- Utilisation directe de l'analyseur.
Hadoop est basé sur le système de construction maven, par conséquent, il n'y avait aucun obstacle avec le chèque.
Après avoir intégré le script de la documentation et édité l'un des fichiers pom.xml (il y avait des modules dans des dépendances qui n'étaient pas disponibles), l'analyse a commencé!
Une fois l'analyse terminée, j'ai choisi les avertissements les plus intéressants et j'ai remarqué que j'avais le même nombre d'avertissements dans le code de production et dans les tests. Normalement, je ne considère pas les avertissements de l'analyseur, donnés pour les tests. Mais quand je les ai divisés, je ne pouvais pas laisser les avertissements «tests» sans surveillance. «Pourquoi ne pas y jeter un coup d'œil?» - pensais-je, car les bogues dans les tests pouvaient également avoir des conséquences néfastes. Ils peuvent conduire à des tests incorrects ou partiels, voire à des méli-mélo (ils existent juste pour cocher la case, ils sont toujours verts).
Après avoir sélectionné les avertissements les plus intrigants, je les ai divisés par les groupes suivants: production, test et les quatre principaux modules Hadoop. Et maintenant, je suis heureux d'offrir à votre attention l'examen des avertissements 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);
La valeur ajoutée deux fois dans
HashSet est un défaut très courant lors de la vérification des projets. Le deuxième ajout sera en fait ignoré. J'espère que cette duplication n'est qu'une tragédie inutile. Et si une autre valeur devait être ajoutée?
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 { ....
Le diagnostic V6072 donne parfois des résultats intéressants. Le but de ce diagnostic est de détecter les fragments de code de même type résultant du copier-coller et du remplacement de une à deux variables. Dans ce cas, certaines variables sont même restées "sous-modifiées".
Le code ci-dessus le démontre. Dans le premier bloc, la variable
localArchives est utilisée, dans le deuxième fragment similaire -
localFiles . Si vous étudiez ce code avec une diligence raisonnable, plutôt que de le parcourir rapidement, comme cela arrive souvent lors de la révision du code, vous remarquerez le fragment, où l'auteur a oublié de remplacer la variable
localArchives .
Cette gaffe peut 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 (new String [localArchives.size ()]) , les 2 derniers éléments seront nuls (["pathToFile1", "pathToFile2", null, null]);
- Ensuite, org.apache.hadoop.util.StringUtils.arrayToString renverra la représentation sous forme de chaîne de notre tableau, dans laquelle les derniers noms de fichiers seront présentés comme "null" ("pathToFile1, pathToFile2, null, null" ) ;
- Tout cela sera passé plus loin et Dieu sait seulement quel genre de 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 le nombre d'éléments est vérifié séparément pour 0, la vérification supplémentaire
children.size ()> 0 sera toujours vraie.
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) {
Ici, le défaut est que la variable est divisée par elle-même. Par conséquent, la vérification de la multiplicité sera toujours réussie et en cas d'obtention d'entrées incorrectes (
windowLenMs ,
numBuckets ), l'exception ne sera jamais 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 = ....;
Mêmes fragments de code dans deux branches de
cas . C'est juste partout! Dans le nombre actuel de cas, il ne s'agit pas d'une véritable erreur, mais simplement d'une raison de penser à une refactorisation du
commutateur . Mais pas pour le cas qui nous occupe. Des fragments de code répétés définissent la valeur de la variable
preemptedVcoreSeconds . Si vous examinez attentivement les noms de toutes les variables et constantes, vous conclurez probablement que si
métric.getId () == APP_MEM_PREEMPT_METRICS, la valeur doit être définie pour la variable
preemptedMemorySeconds , et non pour
preemptedVcoreSeconds . À cet égard, après l'opérateur «switch»,
preemptedMemorySeconds restera toujours 0, tandis que 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); } .... }
La variable
planQueueName n'est pas utilisée lors de la journalisation. Dans ce cas, trop est copié ou la chaîne de formatage n'est pas terminée. Mais je blâme toujours le bon vieux copier-coller, qui dans certains cas est tout simplement génial pour se tirer une balle dans le pied.
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 le V6072. Examinez attentivement les 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? =). Une partie de la boucle, qui fait partie du test lui-même, ne sera jamais exécutée. Cela est dû au fait que les valeurs de compteur initiales et finales sont égales dans l'instruction
for . En conséquence, la condition
i <start deviendra immédiatement fausse, conduisant à un tel comportement. J'ai parcouru le fichier de test et j'ai sauté à la conclusion qu'en réalité,
i <(start + n) devait être écrit dans la condition de boucle.
Mapreduce
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 le comprendre, donnons le code au-dessus d'un autre coup d'œil. Si l'exécution du test atteint l'opération
byteAm - = headerLen , cela signifie que
byteAm> = headerLen. À partir de là, après soustraction, la
valeur octetAm ne sera jamais négative. C'est ce que nous devions 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"); ....
Croyez-le ou non, c'est encore le V6072! Suivez simplement les variables
normalDir et
normalFile.V6027 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 par les mêmes valeurs. Il devrait souvent en être ainsi, mais ce n'est pas le cas. Les noms des variables sont longs et similaires, il n'est donc pas surprenant que le fragment copié-collé n'ait été modifié d'aucune façon. Pour y remédier, lors de l'initialisation de la variable le plus
élevéPriorityLowRedundancyECBlocksStr , l'auteur doit utiliser le paramètre d'entrée le plus
élevéPriorityLowRedundancyECBlocks . En outre, très probablement, ils doivent toujours corriger la ligne 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 se plaint que le compteur
i ++ dans la boucle ne peut pas être modifié. Ce qui signifie que dans la boucle
for (int i = 0; i <slowwriters.length; i ++) {....} pas plus d'une itération ne s'exécutera. Voyons pourquoi. Ainsi, dans la première itération, nous
lions le thread avec le fichier qui correspond à
slowwriters [0] pour une lecture plus approfondie. Ensuite, nous lisons le contenu du fichier via la boucle
pour (int j = 0, x ;; j ++):- si nous lisons quelque chose de pertinent, nous comparons l'octet de lecture avec la valeur actuelle du compteur j via assertEquals (si la vérification échoue, nous échouons au test);
- si le fichier est vérifié avec succès et que nous arrivons à la fin du fichier (nous lisons -1), la méthode se termine.
Par conséquent, quoi qu'il arrive lors de la vérification des
ralentisseurs [0] , il ne sera pas possible de vérifier les éléments suivants. Très probablement, la
pause doit ê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, la méthode
Assert.fail interrompra le test et stacktrace ne sera pas imprimé en cas d'exception. Si le message concernant l'exception interceptée est suffisant ici, il est préférable de supprimer l'impression de stacktrace pour éviter toute confusion. Si l'impression est nécessaire, il vous suffit de les échanger.
De nombreux fragments similaires ont été trouvés:
- 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, V6072 à nouveau =). Variables pour afficher le fragment suspect:
usercache et
publicCache .
Conclusion
Des centaines de milliers de lignes de code sont écrites en cours de développement. Le code de production est généralement conservé à l'abri des bogues, des défauts et des défauts (les développeurs testent leur code, le code est révisé, etc.). Les tests sont nettement inférieurs à cet égard. Les défauts dans les tests peuvent facilement se cacher derrière une "coche verte". Comme vous l'avez probablement compris dans l'examen des avertissements d'aujourd'hui, un test vert n'est pas toujours une vérification réussie.
Cette fois, lors de la vérification de la base de code Apache Hadoop, l'analyse statique s'est avérée très nécessaire à la fois dans le code de production et dans les tests qui jouent également un rôle important dans le développement.
Donc, si vous vous souciez de votre code et de la qualité des tests, je vous suggère de viser l'analyse statique. Que
PVS-Studio soit le premier concurrent de cette entreprise.