在本文中,我們將介紹PVS-Studio靜態分析器對Apache Hadoop程式碼庫的觀察結果。
為了獲得高品質的生產程式碼,僅確保測試的最大覆蓋範圍還不夠。無疑,出色的結果需要主要的專案程式碼和測試才能有效地協同工作。因此,測試必須與原始碼一樣受到重視。體面的測試是成功的關鍵因素,因為它將趕上生產的衰退。讓我們看一下PVS-Studio靜態分析器警告,以檢視測試錯誤並不比生產錯誤更嚴重這一事實的重要性。當今的焦點:Apache Hadoop。
關於該專案那些以前對大資料感興趣的人可能已經聽說過Apache Hadoop專案或與之合作。簡而言之,Hadoop是可以用作構建和使用大資料系統的基礎的框架。
Hadoop由四個主要模組組成;他們每個人都執行Big Dat $$ anonymous $$ nalytics系統所需的特定任務:
Hadoop通用。 MapReduce。 Hadoop分散式檔案系統。 YARN。關於支票如文件中所示,PVS-Studio可以通過多種方式整合到專案中:
使用Maven外掛。 使用Gradle外掛。 使用Gradle IntellJ IDEA。 直接使用分析儀。Hadoop基於Maven構建系統;因此,檢查沒有任何障礙。
在集成了文件中的指令碼並編輯了一個pom.xml檔案(依賴項中有一些模組不可用)之後,分析開始了!
分析完成後,我選擇了最有趣的警告,並注意到在生產程式碼和測試中,我具有相同數量的警告。通常,我不考慮測試中的分析器警告。但是,當我將它們分開時,我無法不理會“測試”警告。我想:“為什麼不看一下它們,因為測試中的錯誤也可能帶來不利的後果。” 它們可能導致錯誤或部分測試,甚至導致雜亂。
生產程式碼Hadoop常見V6033已經添加了具有相同鍵“ KDC_BIND_ADDRESS”的專案。MiniKdc.java(163),MiniKdc.java(162)
Java
1個公共 類 MiniKdc {2 ....3 私有 靜態 最終 Set < String > PROPERTIES = new HashSet < String >();4 ....5 靜態 {6 性質。新增(ORG_NAME);7 性質。新增(ORG_DOMAIN);8 性質。新增(KDC_BIND_ADDRESS);9 性質。新增(KDC_BIND_ADDRESS); // <=10 性質。加(KDC_PORT);11 性質。加(INSTANCE);12 ....13 }14 ....15}
HashSet檢查專案時,a中兩次增加的值 是一個非常常見的缺陷。第二個新增項將被忽略。我希望這種重複只是一場不必要的悲劇。如果要新增另一個值怎麼辦?
MapReduceV6072找到兩個相似的程式碼片段。也許這是一個錯字, localFiles應該使用變數代替 localArchives。
LocalDistributedCacheManager.java(183)。 LocalDistributedCacheManager.java(178)。 LocalDistributedCacheManager.java(176)。LocalDistributedCacheManager.java(181)。Java
1個公共 同步 無效 設定(JobConf conf,JobID jobId)丟擲 IOException {2 ....3 //使用本地化資料更新配置物件。4 如果(!localArchives。的isEmpty()){5 conf。集(MRJobConfig。CACHE_LOCALARCHIVES,StringUtils的6 。arrayToString(localArchives。指定者(新 字串 [ localArchives // <=7 。大小()])));8 }9 如果(!localFiles。的isEmpty()){10 conf。集(MRJobConfig。CACHE_LOCALFILES,StringUtils的11 。arrayToString(localFiles。指定者(新 字串 [ localArchives // <=12 。大小()])));13 }14 ....15}
V6072診斷程式有時會產生一些有趣的發現。此診斷的目的是檢測由於複製貼上和替換兩個變數而導致的相同型別的程式碼片段。在這種情況下,某些變數甚至保持“不變”。
上面的程式碼演示了這一點。在第一個塊中, localArchives變數用於第二個類似的片段 localFiles。如果您認真研究此程式碼,而不是很快進行遍歷,這通常在程式碼審閱時發生,那麼您會注意到該片段,作者忘記了替換該 localArchives變數。
這種失態可能導致以下情況:
假設我們有localArchives(大小= 4)和localFiles(大小= 2)。 建立陣列時 localFiles.toArray(new String[localArchives.size()]),最後兩個元素將為null(["pathToFile1", "pathToFile2", null, null])。然後, org.apache.hadoop.util.StringUtils.arrayToString將返回陣列的字串表示形式,其中最後的檔名將顯示為“ null”(“ pathToFile1,pathToFile2,null,null”)。 所有這些都將進一步傳遞,上帝只知道這種情況下會有什麼樣的檢查。V6007表示式'children.size()> 0'始終為true。Queue.java(347)
Java
1個boolean isHierarchySameAs(Queue newState){2 ....3 如果(孩子 == 空 || 孩子。大小()== 0){4 ....5 }6 否則 ,如果(孩子。大小()> 0)7 {8 ....9 }10 ....11}
由於要單獨檢查元素數是否為0,因此進一步檢查children.size()> 0將始終為true。
HDFSV6001在'%'運算子的左側和右側有相同的子表示式'this.bucketSize'。RollingWindow.java(79)。
Java
1個 RollingWindow(int windowLenMs,int numBuckets){2 buckets = new Bucket [ numBuckets ];3 for(int i = 0 ; i < numBuckets ; i ++){4 buckets [ i ] = new Bucket();5 }6 這個。windowLenMs = windowLenMs ;7 這個。bucketSize = windowLenMs / numBuckets ;8 如果(此。bucketSize % bucketSize != 0){ // <=9 丟擲 新的 IllegalArgumentException(10 “滾動視窗中的儲存桶大小不是整數:windowLenMs =”11 + windowLenMs + “ numBuckets =”“ + numBuckets);12 }13 }
YARN
V6067兩個或更多案例分支執行相同的操作。TimelineEntityV2Converter.java(386),TimelineEntityV2Converter.java(389)。
Java
1個 公共 靜態 ApplicationReport2 convertToApplicationReport(TimelineEntity 實體)3{4 ....5 如果(指標 != null){6 long vcoreSeconds = 0 ;7 long memorySeconds = 0 ;8 long preemptedVcoreSeconds = 0 ;9 long preemptedMemorySeconds = 0 ;1011 對於(TimelineMetric 指標:指標){12 開關(度量。的getId()){13 case ApplicationMetricsConstants。APP_CPU_METRICS:14 vcoreSeconds = getAverageValue(度量。的GetValues。()的值());15 休息 ;16 case ApplicationMetricsConstants。APP_MEM_METRICS:17 memorySeconds = ....;18歲 休息 ;19 case ApplicationMetricsConstants。APP_MEM_PREEMPT_METRICS:20 preemptedVcoreSeconds = ....; // <=21 休息 ;22 case ApplicationMetricsConstants。APP_CPU_PREEMPT_METRICS:23 preemptedVcoreSeconds = ....; // <=24 休息 ;25 預設值:26 //不應該發生27 休息 ;28 }29 }30 ....31 }32 ....33}
相同的程式碼片段位於兩個case分支中。到處都是!在大多數情況下,這不是真正的錯誤,而只是考慮重構switch語句的原因。對於當前的情況,情況並非如此。重複的程式碼片段設定變數的值preemptedVcoreSeconds。如果仔細檢視所有變數和常量的名稱,可能會得出結論,在這種情況下, if metric.getId() == APP_MEM_PREEMPT_METRICS必須為preemptedMemorySeconds變數設定 值,而不是 preemptedVcoreSeconds。在這方面,在switch語句之後,preemptedMemorySeconds將始終保持0,而的值 preemptedVcoreSeconds可能不正確。
V6046格式錯誤。期望使用不同數量的格式項。不使用的引數:2. AbstractSchedulerPlanFollower.java(186)
Java
1個@Override2市民 同步 無效 synchronizePlan(規劃 計劃,布林 shouldReplan)3{4 ....5 嘗試6 {7 setQueueEntitlement(planQueueName,....);8 }9 捕獲(YarnException e)10 {11 LOG。警告(“嘗試為計劃{{}確定保留大小時發生異常”,12 currResId,13 planQueueName,14 e);15 }16 ....17}
planQueueName記錄時不使用該 變數。在這種情況下,要麼複製太多,要麼格式字串未完成。但是我仍然要責備舊的複製貼上,在某些情況下,將其貼上在腳上真是太好了。
測試程式碼Hadoop常見V6072找到兩個相似的程式碼片段。也許這是一個錯字,應該使用'allSecretsB'變數而不是'allSecretsA'。
TestZKSignerSecretProvider.java(316),TestZKSignerSecretProvider.java(309),TestZKSignerSecretProvider.java(306),TestZKSignerSecretProvider.java(313)。
Java
1個public void testMultiple(整數 順序)引發 異常 {2 ....3 currentSecretA = secretProviderA。getCurrentSecret();4 allSecretsA = secretProviderA。getAllSecrets();5 斷言。assertArrayEquals(secretA2,currentSecretA);6 斷言。的assertEquals(2,allSecretsA。長度); // <=7 斷言。assertArrayEquals(secretA2,allSecretsA [ 0 ]);8 斷言。assertArrayEquals(secretA1,allSecretsA [ 1 ]);910 currentSecretB = secretProviderB。getCurrentSecret();11 allSecretsB = secretProviderB。getAllSecrets();12 斷言。assertArrayEquals(secretA2,currentSecretB);13 斷言。的assertEquals(2,allSecretsA。長度); // <=14 斷言。assertArrayEquals(secretA2,allSecretsB [ 0 ]);15 斷言。assertArrayEquals(secretA1,allSecretsB [ 1 ]);16 ....17}
再次是V6072。仔細觀察變數allSecretsA和allSecretsB。
V6043考慮檢查“ for”運算子。迭代器的初始值和最終值相同。TestTFile.java(235)。
Java
1個私有 int readPrepWithUnknownLength(掃描 儀掃描器,int start,int n)2 引發 IOException {3 對於(int i = start ; i < start ; i ++){4 字串 鍵 = 字串。格式(localFormatter,i);5 位元組 [] 讀取 = readKey(掃描器);6 assertTrue(“鍵不等於”,陣列。等號(鍵。的getBytes(),讀));7 嘗試 {8 讀取 = 讀取值(掃描器);9 assertTrue(false);10 }11 抓(IOException 即){12 //應該丟擲異常13 }14 字串 值 = “值” + 鍵;15 讀取 = readLongValue(掃描器,值。的getBytes()。長度);16 assertTrue(“n要相等的值”,陣列。等號(讀,值。的getBytes()));17 掃描器。前進();18歲 }19 返回(start + n);20}
始終是綠色的測試?=)。迴圈的一部分,即測試本身的一部分,將永遠不會執行。這是由於以下事實:for語句中的初始計數器值和最終計數器值相等 。結果,條件 i < start將立即變為假,從而導致這種行為。我瀏覽了測試檔案,然後得出i < (start + n)必須在迴圈條件下編寫的結論 。
MapReduceV6007表示式'byteAm <0'始終為false。DataWriter.java(322)
Java
1個GenerateOutput writeSegment(long byteAm,OutputStream out)2 引發 IOException {3 long headerLen = getHeaderLength();4 if(byteAm < headerLen){5 //沒有足夠的位元組寫頭6 返回 新 GenerateOutput(0,0);7 }8 //調整標題長度9 byteAm- = headerLen ;10 if(byteAm < 0){ // <=11 byteAm = 0 ;12 }13 ....14}
條件 byteAm < 0始終為假。為了弄清楚,讓我們在上面的程式碼再看一遍。如果測試執行達到了操作的要求 byteAm -= headerLen,則意味著 byteAm >= headerLen。從這裡開始,減去後,該 byteAm值將永遠不會為負。那就是我們必須證明的。
HDFSV6072找到兩個相似的程式碼片段。也許這是一個錯字, normalFile應該使用變數代替 normalDir。TestWebHDFS.java(625),TestWebHDFS.java(615),TestWebHDFS.java(614),TestWebHDFS.java(624)
Java
1個public void testWebHdfsErasureCodingFiles()引發 異常 {2 ....3 最終 路徑 normalDir = 新 路徑(“ / dir”);4 dfs。mkdirs(normalDir);5 最終 路徑 normalFile = 新 路徑(normalDir,“ file.log ”);6 ....7 //邏輯塊#18 時間filestatus expectedNormalDirStatus = DFS。getFileStatus(normalDir);9 FileStatus actualNormalDirStatus = webHdfs。getFileStatus(normalDir); // <=10 斷言。的assertEquals(expectedNormalDirStatus。isErasureCoded(),11 actualNormalDirStatus。isErasureCoded());12 ContractTestUtils。assertNotErasureCoded(dfs,normalDir);13 assertTrue(normalDir + “應具有擦除編碼中未設定” + ....);1415 //邏輯塊#216 時間filestatus expectedNormalFileStatus = DFS。getFileStatus(normalFile);17 FileStatus actualNormalFileStatus = webHdfs。getFileStatus(normalDir); // <=18歲 斷言。的assertEquals(expectedNormalFileStatus。isErasureCoded(),19 actualNormalFileStatus。isErasureCoded());20 ContractTestUtils。assertNotErasureCoded(dfs,normalFile);21 assertTrue(normalFile + “應具有擦除編碼中未設定” + ....);22}
信不信由你,它又是V6072!只需跟隨變數 normalDir和 normalFile。
V6027通過呼叫同一函式來初始化變數。可能是錯誤或未優化的程式碼。TestDFSAdmin.java(883),TestDFSAdmin.java(879)。
Java
1個私人 void verifyNodesAndCorruptBlocks(2 最終 整數 numDn,3 final int numLiveDn,4 final int numCorruptBlocks,5 final int numCorruptECBlockGroups,6 最終的 DFSClient 客戶端,7 最後的 long 最高PriorityLowRedundancyReplicatedBlocks,8 最後的 Long 最高PriorityLowRedundancyECBlocks)9 引發 IOException10{11 / *初始化變數* /12 ....13 最後的 字串 ExpectedCorruptedECBlockGroupsStr = String。格式(14 “具有損壞的內部塊的塊組:%d”,15 numCorruptECBlockGroups);16 最後的 字串的 highestPriorityLowRedundancyReplicatedBlocksStr17 = 字串。格式(18歲 “ \\ t具有最高優先順序的低冗餘塊” +19 “要恢復:%d”,20 maximumPriorityLowRedundancyReplicatedBlocks);21 最後的 字串 highestPriorityLowRedundancyECBlocksStr = String。格式(22 “ \\ t具有最高優先順序的低冗餘塊” +23 “要恢復:%d”,24 maximumPriorityLowRedundancyReplicatedBlocks);25 ....26}
在這個片段中,highestPriorityLowRedundancyReplicatedBlocksStr和highestPriorityLowRedundancyECBlocksStr用相同的值初始化。通常應該是這樣,但事實並非如此。變數的名稱又長又相似,因此複製貼上的片段沒有進行任何更改也就不足為奇了。為了解決這個問題,在初始化highestPriorityLowRedundancyECBlocksStr變數時,作者必須使用輸入引數highestPriorityLowRedundancyECBlocks。此外,最有可能的是,他們仍然需要更正格式行。
V6019檢測不到程式碼。可能存在錯誤。TestReplaceDatanodeFailureReplication.java(222)。
Java
1個私人 虛空2verifyFileContent(....,SlowWriter [] slowwriters)引發 IOException3{4 LOG。資訊(“驗證檔案”);5 對(INT 我 = 0 ; 我 < slowwriters。長度 ; 我++){6 LOG。資訊(slowwriters [ 我 ]。檔案路徑 + ....);7 FSDataInputStream in = null ;8 嘗試 {9 in = fs。開放(slowwriters [ 我 ]。檔案路徑);10 for(int j = 0,x ;; j ++){11 x = in中。閱讀();12 如果((x)!= - 1){13 斷言。assertEquals(j,x);14 } 其他 {15 回報 ;16 }17 }18歲 } 最後 {19 IOUtils。closeStream(in);20 }21 }22}
分析儀抱怨i++無法更改迴圈中的計數器。這意味著在for (int i = 0; i < slowwriters.length; i++) {....}迴圈中最多將執行一次迭代。讓我們找出原因。在第一次迭代中,我們將執行緒與對應的檔案連結起來,以slowwriters[0]供進一步閱讀。接下來,我們通過loop讀取檔案內容for (int j = 0, x;; j++)。
如果我們讀取了一些相關的內容,我們會將讀取的位元組與j計數器的當前值進行比較assertEquals(如果檢查不成功,則測試失敗)。 如果檔案檢查成功,並且到達檔案末尾(讀取為-1),則該方法退出。因此,無論在檢查期間發生什麼 slowwriters[0],都不會去檢查後續元素。最有可能的是break,必須使用a代替return。
YARNV6019檢測不到程式碼。可能存在錯誤。TestNodeManager.java(176)
Java
1個@測試2公共 無效 3testCreationOfNodeLabelsProviderService()引發 InterruptedException {4 嘗試 {5 ....6 } catch(異常 e){7 斷言。失敗(“捕獲到異常”);8 e。printStackTrace();9 }10}
在這種情況下,該 Assert.fail方法將中斷測試,並且在發生異常的情況下不會列印堆疊跟蹤。如果有關捕獲到的異常的訊息在這裡足夠多,則最好刪除列印堆疊跟蹤的記錄,以免造成混淆。如果需要列印,則只需交換它們。
已發現許多類似的片段:
V6019檢測不到程式碼。可能存在錯誤。TestResourceTrackerService.java(928)。 V6019檢測不到程式碼。可能存在錯誤。TestResourceTrackerService.java(737)。 V6019檢測不到程式碼。可能存在錯誤。TestResourceTrackerService.java(685)。V6072找到兩個相似的程式碼片段。也許這是一個錯字, publicCache應該使用變數代替 usercache。
TestResourceLocalizationService.java(315),
TestResourceLocalizationService.java(309),
TestResourceLocalizationService.java(307),
TestResourceLocalizationService.java(313)
Java
1個@測試2公共 無效 testDirectoryCleanupOnNewlyCreatedStateStore()3 丟擲 IOException,URISyntaxException4{5 ....6 //驗證目錄建立7 對於(路徑 p:localDirs){8 p = 新 路徑((新 URI(p。的toString()))。的getPath());910 //邏輯塊#111 路徑 usercache = 新 路徑(p,ContainerLocalizer。USERCACHE);12 驗證(spylfs)。重新命名(當量(usercache),任何(路徑。類),任何()); // <=13 驗證(spylfs)。mkdir(eq(usercache),....);1415 //邏輯塊#216 路徑 publicCache = 新 路徑(p,ContainerLocalizer。FILECACHE);17 驗證(spylfs)。重新命名(當量(usercache),任何(路徑。類),任何()); // <=18歲 驗證(spylfs)。mkdir(eq(publicCache),....);19 ....20 }21 ....22}
最後,再次是V6072 =)。用於檢視可疑片段的變數: usercache 和publicCache。
結論開發中編寫了成千上萬行程式碼。生產程式碼通常保持清潔,沒有錯誤,缺陷和缺陷(開發人員測試他們的程式碼,檢查程式碼等)。在這方面,測試肯定不如。測試中的缺陷很容易隱藏在“綠色刻度”後面。正如您可能從今天的警告回顧中了解到的那樣,綠色測試並不總是一項成功的檢查。
這次,當檢查Apache Hadoop程式碼庫時,在生產程式碼和測試中都非常需要靜態分析,而靜態分析在開發中也起著重要作用。因此,如果您關心程式碼和測試品質,建議您著眼於靜態分析。
原文:https://dzone.com/articles/apache-hadoop-code-quality-production-vs-test