Apache Hadoop代碼質(zhì)量:生產(chǎn)VS測試
為了獲得高質(zhì)量的生產(chǎn)代碼,僅確保測試的最大覆蓋范圍還不夠。無疑,出色的結(jié)果需要主要的項目代碼和測試才能有效地協(xié)同工作。因此,測試必須與源代碼一樣受到重視。體面的測試是成功的關(guān)鍵因素,因為它將趕上生產(chǎn)的衰退。讓我們看一下PVS-Studio靜態(tài)分析器警告,以查看測試錯誤并不比生產(chǎn)錯誤更嚴重這一事實的重要性。當今的焦點:Apache Hadoop。

關(guān)于該項目
那些以前對大數(shù)據(jù)感興趣的人可能已經(jīng)聽說過Apache Hadoop項目或與之合作。簡而言之,Hadoop是可以用作構(gòu)建和使用大數(shù)據(jù)系統(tǒng)的基礎(chǔ)的框架。
Hadoop由四個主要模塊組成;他們每個人都執(zhí)行Big Dat $$ anonymous $$ nalytics系統(tǒng)所需的特定任務:
- Hadoop通用。
- MapReduce。
- Hadoop分布式文件系統(tǒng)。
- YARN。
關(guān)于支票
如文檔中所示,PVS-Studio可以通過多種方式集成到項目中:
- 使用Maven插件。
- 使用Gradle插件。
- 使用Gradle IntellJ IDEA。
- 直接使用分析儀。
Hadoop基于Maven構(gòu)建系統(tǒng);因此,檢查沒有任何障礙。
在集成了文檔中的腳本并編輯了一個pom.xml文件(依賴項中有一些模塊不可用)之后,分析開始了!
分析完成后,我選擇了最有趣的警告,并注意到在生產(chǎn)代碼和測試中,我具有相同數(shù)量的警告。通常,我不考慮測試中的分析器警告。但是,當我將它們分開時,我無法不理會“測試”警告。我想:“為什么不看一下它們,因為測試中的錯誤也可能帶來不利的后果。” 它們可能導致錯誤或部分測試,甚至導致雜亂。
選擇最有趣的警告后,我將其分為以下幾類:生產(chǎn),測試和四個主要Hadoop模塊?,F(xiàn)在,我很高興對分析儀警告進行回顧。
生產(chǎn)代碼
Hadoop常見
V6033已經(jīng)添加了具有相同鍵“ KDC_BIND_ADDRESS”的項目。MiniKdc.java(163),MiniKdc.java(162)
Java
- 1個公共 類 MiniKdc {2 ....3 私有 靜態(tài) 最終 Set < String >
- PROPERTIES = new HashSet < String >();4 ....5 靜態(tài) {6 性質(zhì)。
- 添加(ORG_NAME);7 性質(zhì)。添加(ORG_DOMAIN);8 性質(zhì)。添加
- (KDC_BIND_ADDRESS);9 性質(zhì)。添加(KDC_BIND_ADDRESS); // <=10 性
- 質(zhì)。加(KDC_PORT);11 性質(zhì)。加(INSTANCE);12 ....13 }14 ....15}
HashSet檢查項目時,a中兩次增加的值 是一個非常常見的缺陷。第二個添加項將被忽略。我希望這種重復只是一場不必要的悲劇。如果要添加另一個值怎么辦?
MapReduce
V6072找到兩個相似的代碼片段。也許這是一個錯字, localFiles應該使用變量代替 localArchives。
- LocalDistributedCacheManager.java(183)。
- LocalDistributedCacheManager.java(178)。
- LocalDistributedCacheManager.java(176)。
- LocalDistributedCacheManager.java(181)。
Java
- 1個公共 同步 無效 設(shè)置(JobConf conf,JobID jobId)拋出 IOException
- {2 ....3 //使用本地化數(shù)據(jù)更新配置對象。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診斷程序有時會產(chǎn)生一些有趣的發(fā)現(xiàn)。此診斷的目的是檢測由于復制粘貼和替換兩個變量而導致的相同類型的代碼片段。在這種情況下,某些變量甚至保持“不變”。
上面的代碼演示了這一點。在第一個塊中, localArchives變量用于第二個類似的片段 localFiles。如果您認真研究此代碼,而不是很快進行遍歷,這通常在代碼審閱時發(fā)生,那么您會注意到該片段,作者忘記了替換該 localArchives變量。
這種失態(tài)可能導致以下情況:
- 假設(shè)我們有l(wèi)ocalArchives(大小= 4)和localFiles(大小= 2)。
- 創(chuàng)建數(shù)組時 localFiles.toArray(new String[localArchives.size()]),最后兩個元素將為null(["pathToFile1", "pathToFile2", null, null])。
- 然后, org.apache.hadoop.util.StringUtils.arrayToString將返回數(shù)組的字符串表示形式,其中最后的文件名將顯示為“ 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 否則 ,如果(孩子。大
- ?。ǎ?gt; 0)7 {8 ....9 }10 ....11}
由于要單獨檢查元素數(shù)是否為0,因此進一步檢查children.size()> 0將始終為true。
HDFS
V6001在'%'運算符的左側(cè)和右側(cè)有相同的子表達式'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 “滾動窗口中的存儲桶大小不是整
- 數(shù):windowLenMs =”11 + windowLenMs + “ numBuckets =”“ +
- numBuckets);12 }13 }
YARN
V6067兩個或更多案例分支執(zhí)行相同的操作。TimelineEntityV2Converter.java(386),TimelineEntityV2Converter.java(389)。
Java
- 1個 公共 靜態(tài) 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 開關(guān)(度量。的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 //不應該發(fā)生27 休息 ;28 }29 }30 ....31
- }32 ....33}
相同的代碼片段位于兩個case分支中。到處都是!在大多數(shù)情況下,這不是真正的錯誤,而只是考慮重構(gòu)switch語句的原因。對于當前的情況,情況并非如此。重復的代碼片段設(shè)置變量的值preemptedVcoreSeconds。如果仔細查看所有變量和常量的名稱,可能會得出結(jié)論,在這種情況下, if metric.getId() == APP_MEM_PREEMPT_METRICS必須為preemptedMemorySeconds變量設(shè)置 值,而不是 preemptedVcoreSeconds。在這方面,在switch語句之后,preemptedMemorySeconds將始終保持0,而的值 preemptedVcoreSeconds可能不正確。
V6046格式錯誤。期望使用不同數(shù)量的格式項。不使用的參數(shù):2. AbstractSchedulerPlanFollower.java(186)
Java
- 1個@Override2市民 同步 無效 synchronizePlan(規(guī)劃 計劃,布爾
- shouldReplan)3{4 ....5 嘗試6 {7
- setQueueEntitlement(planQueueName,....);8 }9 捕獲(YarnException
- e)10 {11 LOG。警告(“嘗試為計劃{{}確定保留大小時發(fā)生異常”,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(整數(shù) 順序)引發(fā) 異常 {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 引發(fā) IOException {3 對于(int i = start ; i <
- start ; i ++){4 字符串 鍵 = 字符串。格式(localFormatter,i);5
- 字節(jié) [] 讀取 = 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}
始終是綠色的測試?=)。循環(huán)的一部分,即測試本身的一部分,將永遠不會執(zhí)行。這是由于以下事實:for語句中的初始計數(shù)器值和最終計數(shù)器值相等 。結(jié)果,條件 i < start將立即變?yōu)榧?,從而導致這種行為。我瀏覽了測試文件,然后得出i < (start + n)必須在循環(huán)條件下編寫的結(jié)論 。
MapReduce
V6007表達式'byteAm <0'始終為false。DataWriter.java(322)
Java
- 個GenerateOutput writeSegment(long byteAm,OutputStream out)2
- 引發(fā) IOException {3 long headerLen = getHeaderLength();4
- if(byteAm < headerLen){5 //沒有足夠的字節(jié)寫頭6 返回 新
- GenerateOutput(0,0);7 }8 //調(diào)整標題長度9 byteAm- = headerLen
- ;10 if(byteAm < 0){ // <=11 byteAm = 0 ;12 }13 ....14}
條件 byteAm < 0始終為假。為了弄清楚,讓我們在上面的代碼再看一遍。如果測試執(zhí)行達到了操作的要求 byteAm -= headerLen,則意味著 byteAm >= headerLen。從這里開始,減去后,該 byteAm值將永遠不會為負。那就是我們必須證明的。
HDFS
V6072找到兩個相似的代碼片段。也許這是一個錯字, normalFile應該使用變量代替 normalDir。TestWebHDFS.java(625),TestWebHDFS.java(615),TestWebHDFS.java(614),TestWebHDFS.java(624)
Java
- 1個public void testWebHdfsErasureCodingFiles()引發(fā) 異常 {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 + “應具有擦除編碼中未設(shè)置” +
- ....);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
- + “應具有擦除編碼中未設(shè)置” + ....);22}
信不信由你,它又是V6072!只需跟隨變量 normalDir和 normalFile。
V6027通過調(diào)用同一函數(shù)來初始化變量。可能是錯誤或未優(yōu)化的代碼。TestDFSAdmin.java(883),TestDFSAdmin.java(879)。
Java
- 1個私人 void verifyNodesAndCorruptBlocks(2 最終 整數(shù) numDn,3
- final int numLiveDn,4 final int numCorruptBlocks,5 final
- int numCorruptECBlockGroups,6 最終的 DFSClient 客戶端,7 最后的
- long 最高PriorityLowRedundancyReplicatedBlocks,8 最后的 Long 最高
- PriorityLowRedundancyECBlocks)9 引發(fā) IOException10{11 / *初始化變
- 量* /12 ....13 最后的 字符串 ExpectedCorruptedECBlockGroupsStr =
- String。格式(14 “具有損壞的內(nèi)部塊的塊組:%d”,15
- numCorruptECBlockGroups);16 最后的 字符串的
- highestPriorityLowRedundancyReplicatedBlocksStr17 = 字符串。格式
- (18歲 “ \ t具有最高優(yōu)先級的低冗余塊” +19 “要恢
- 復:%d”,20 maximumPriorityLowRedundancyReplicatedBlocks);21 最
- 后的 字符串 highestPriorityLowRedundancyECBlocksStr = String。格式
- (22 “ \ t具有最高優(yōu)先級的低冗余塊” +23 “要恢復:%d”,24
- maximumPriorityLowRedundancyReplicatedBlocks);25 ....26}
在這個片段中,highestPriorityLowRedundancyReplicatedBlocksStr和highestPriorityLowRedundancyECBlocksStr用相同的值初始化。通常應該是這樣,但事實并非如此。變量的名稱又長又相似,因此復制粘貼的片段沒有進行任何更改也就不足為奇了。為了解決這個問題,在初始化highestPriorityLowRedundancyECBlocksStr變量時,作者必須使用輸入?yún)?shù)highestPriorityLowRedundancyECBlocks。此外,最有可能的是,他們?nèi)匀恍枰袷叫小?/p>
V6019檢測不到代碼??赡艽嬖阱e誤。TestReplaceDatanodeFailureReplication.java(222)。
Java
- 1個私人 虛空2verifyFileContent(....,SlowWriter [] slowwriters)引發(fā)
- 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++無法更改循環(huán)中的計數(shù)器。這意味著在for (int i = 0; i < slowwriters.length; i++) {....}循環(huán)中最多將執(zhí)行一次迭代。讓我們找出原因。在第一次迭代中,我們將線程與對應的文件鏈接起來,以slowwriters[0]供進一步閱讀。接下來,我們通過loop讀取文件內(nèi)容for (int j = 0, x;; j++)。
如果我們讀取了一些相關(guān)的內(nèi)容,我們會將讀取的字節(jié)與j計數(shù)器的當前值進行比較assertEquals(如果檢查不成功,則測試失敗)。
如果文件檢查成功,并且到達文件末尾(讀取為-1),則該方法退出。
因此,無論在檢查期間發(fā)生什么 slowwriters[0],都不會去檢查后續(xù)元素。最有可能的是break,必須使用a代替return。
YARN
V6019檢測不到代碼??赡艽嬖阱e誤。TestNodeManager.java(176)
Java
- 1個@測試2公共 無效 3testCreationOfNodeLabelsProviderService()引發(fā)
- InterruptedException {4 嘗試 {5 ....6 } catch(異常 e){7 斷言。
- 失?。?ldquo;捕獲到異常”);8 e。printStackTrace();9 }10}
在這種情況下,該 Assert.fail方法將中斷測試,并且在發(fā)生異常的情況下不會打印堆棧跟蹤。如果有關(guān)捕獲到的異常的消息在這里足夠多,則最好刪除打印堆棧跟蹤的記錄,以免造成混淆。如果需要打印,則只需交換它們。
已發(fā)現(xiàn)許多類似的片段:
- V6019檢測不到代碼??赡艽嬖阱e誤。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 //驗證目錄創(chuàng)建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。
結(jié)論
開發(fā)中編寫了成千上萬行代碼。生產(chǎn)代碼通常保持清潔,沒有錯誤,缺陷和缺陷(開發(fā)人員測試他們的代碼,檢查代碼等)。在這方面,測試肯定不如。測試中的缺陷很容易隱藏在“綠色刻度”后面。正如您可能從今天的警告回顧中了解到的那樣,綠色測試并不總是一項成功的檢查。
這次,當檢查Apache Hadoop代碼庫時,在生產(chǎn)代碼和測試中都非常需要靜態(tài)分析,而靜態(tài)分析在開發(fā)中也起著重要作用。因此,如果您關(guān)心代碼和測試質(zhì)量,建議您著眼于靜態(tài)分析。