01.13 Apache Hadoop代碼質量:生產VS測試

在本文中,我們將介紹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模塊。現在,我很高興對分析儀警告進行回顧。

生產代碼

Hadoop常見

V6033已經添加了具有相同鍵“ KDC_BIND_ADDRESS”的項目。MiniKdc.java(163),MiniKdc.java(162)

Java

<code>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}/<code>

HashSet檢查項目時,a中兩次增加的值 是一個非常常見的缺陷。第二個添加項將被忽略。我希望這種重複只是一場不必要的悲劇。如果要添加另一個值怎麼辦?

MapReduce

V6072找到兩個相似的代碼片段。也許這是一個錯字, localFiles應該使用變量代替 localArchives。

LocalDistributedCacheManager.java(183)。 LocalDistributedCacheManager.java(178)。 LocalDistributedCacheManager.java(176)。LocalDistributedCacheManager.java(181)。

Java

<code>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}/<code>

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

<code>1個boolean isHierarchySameAs(Queue newState){2 ....3 如果(孩子 == 空 || 孩子。大小()== 0){4 ....5 }6 否則 ,如果(孩子。大小()> 0)7 {8 ....9 }10 ....11}/<code>


由於要單獨檢查元素數是否為0,因此進一步檢查children.size()> 0將始終為true。

HDFS

V6001在'%'運算符的左側和右側有相同的子表達式'this.bucketSize'。RollingWindow.java(79)。

Java

<code>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 }/<code>

YARN

V6067兩個或更多案例分支執行相同的操作。TimelineEntityV2Converter.java(386),TimelineEntityV2Converter.java(389)。

Java

<code>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}/<code>


相同的代碼片段位於兩個case分支中。到處都是!在大多數情況下,這不是真正的錯誤,而只是考慮重構switch語句的原因。對於當前的情況,情況並非如此。重複的代碼片段設置變量的值preemptedVcoreSeconds。如果仔細查看所有變量和常量的名稱,可能會得出結論,在這種情況下, if metric.getId() == APP_MEM_PREEMPT_METRICS必須為preemptedMemorySeconds變量設置 值,而不是 preemptedVcoreSeconds。在這方面,在switch語句之後,preemptedMemorySeconds將始終保持0,而的值 preemptedVcoreSeconds可能不正確。

V6046格式錯誤。期望使用不同數量的格式項。不使用的參數:2. AbstractSchedulerPlanFollower.java(186)

Java

<code>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}/<code>


planQueueName記錄時不使用該 變量。在這種情況下,要麼複製太多,要麼格式字符串未完成。但是我仍然要責備舊的複製粘貼,在某些情況下,將其粘貼在腳上真是太好了。

測試代碼

Hadoop常見

V6072找到兩個相似的代碼片段。也許這是一個錯字,應該使用'allSecretsB'變量而不是'allSecretsA'。

TestZKSignerSecretProvider.java(316),TestZKSignerSecretProvider.java(309),TestZKSignerSecretProvider.java(306),TestZKSignerSecretProvider.java(313)。

Java

<code>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}/<code>


再次是V6072。仔細觀察變量allSecretsA和allSecretsB。

V6043考慮檢查“ for”運算符。迭代器的初始值和最終值相同。TestTFile.java(235)。

Java

<code>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}/<code>


始終是綠色的測試?=)。循環的一部分,即測試本身的一部分,將永遠不會執行。這是由於以下事實:for語句中的初始計數器值和最終計數器值相等 。結果,條件 i < start將立即變為假,從而導致這種行為。我瀏覽了測試文件,然後得出i < (start + n)必須在循環條件下編寫的結論 。

MapReduce

V6007表達式'byteAm <0'始終為false。DataWriter.java(322)

Java

<code>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}/<code>

條件 byteAm < 0始終為假。為了弄清楚,讓我們在上面的代碼再看一遍。如果測試執行達到了操作的要求 byteAm -= headerLen,則意味著 byteAm >= headerLen。從這裡開始,減去後,該 byteAm值將永遠不會為負。那就是我們必須證明的。

HDFS

V6072找到兩個相似的代碼片段。也許這是一個錯字, normalFile應該使用變量代替 normalDir。TestWebHDFS.java(625),TestWebHDFS.java(615),TestWebHDFS.java(614),TestWebHDFS.java(624)

Java

<code>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}/<code>

信不信由你,它又是V6072!只需跟隨變量 normalDir和 normalFile。

V6027通過調用同一函數來初始化變量。可能是錯誤或未優化的代碼。TestDFSAdmin.java(883),TestDFSAdmin.java(879)。

Java

<code>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}/<code>

在這個片段中,highestPriorityLowRedundancyReplicatedBlocksStr和highestPriorityLowRedundancyECBlocksStr用相同的值初始化。通常應該是這樣,但事實並非如此。變量的名稱又長又相似,因此複製粘貼的片段沒有進行任何更改也就不足為奇了。為了解決這個問題,在初始化highestPriorityLowRedundancyECBlocksStr變量時,作者必須使用輸入參數highestPriorityLowRedundancyECBlocks。此外,最有可能的是,他們仍然需要更正格式行。

V6019檢測不到代碼。可能存在錯誤。TestReplaceDatanodeFailureReplication.java(222)。

Java

<code>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}/<code>


分析儀抱怨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。

YARN

V6019檢測不到代碼。可能存在錯誤。TestNodeManager.java(176)

Java

<code>1個@測試2公共 無效 3testCreationOfNodeLabelsProviderService()引發 InterruptedException {4 嘗試 {5 ....6 } catch(異常 e){7 斷言。失敗(“捕獲到異常”);8 e。printStackTrace();9 }10}/<code>

在這種情況下,該 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

<code>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}/<code>


最後,再次是V6072 =)。用於查看可疑片段的變量: usercache 和publicCache。

結論

開發中編寫了成千上萬行代碼。生產代碼通常保持清潔,沒有錯誤,缺陷和缺陷(開發人員測試他們的代碼,檢查代碼等)。在這方面,測試肯定不如。測試中的缺陷很容易隱藏在“綠色刻度”後面。正如您可能從今天的警告回顧中瞭解到的那樣,綠色測試並不總是一項成功的檢查。


這次,當檢查Apache Hadoop代碼庫時,在生產代碼和測試中都非常需要靜態分析,而靜態分析在開發中也起著重要作用。因此,如果您關心代碼和測試質量,建議您著眼於靜態分析。

原文:https://dzone.com/articles/apache-hadoop-code-quality-production-vs-test