代碼覆蓋率是衡量軟件測試完成情況的指標,通常基于測試過程中已檢查的程序源代碼比例計算得出。代碼覆蓋率可以有效避免包含未測試代碼的程序被發布。
1. 問題背景
代碼覆蓋(Code )是軟件測試中的一種度量,描述程式中源代碼被測試的比例和程度,所得比例稱為代碼覆蓋率。
在進行代碼測試時,常常使用代碼覆蓋率作為考核測試任務完整性的指標,并且代碼覆蓋率也被拿來作為衡量代碼質量的度量,甚至客戶常常要求交付的軟件達到一定的代碼覆蓋率才能進行發布,因此代碼覆蓋率統計尤為重要。
C語言嵌入式軟件的開發與普通的軟件的開發很大的不同點就是需要采用交叉開發的方式,即開發工具運行在軟硬件配置豐富的編譯機上,而嵌入式應用程序則運行在軟硬件資源相對缺乏的目標機上。面對C語言的覆蓋率工具相對java等語言較少,而對嵌入式軟件交叉編譯后的工具更是鳳毛麟角,所以嵌入式軟件的代碼覆蓋率就成為了一個難題。
2. 解決方法
2.1 覆蓋率工具
嵌入式開發一般使用GNU/GCC作為主要的編譯器,GCOV是一個GNU/GCC的配套測試覆蓋率的工具,是一款的免費的代碼覆蓋率測試工具,而且可以結合LCOV生成美觀的html的測試報表。當對目標代碼進行測試后,GCOV編譯插樁后的程序會監視目標代碼的執行情況,記錄執行的代碼行和未執行的代碼行,并可以記錄某代碼行的執行次數,為分析代碼的執行效率提供依據。
LCOV是GCOV的一個擴展工具,該擴展工具由一套Perl腳本組成,使基于GCOV的文本式輸出實現了一下的增強的功能:
1.基于html的輸出,使用條形圖和不同的顏色來表。
2.支持大型項目,信息匯總頁面提供三個層次的代碼覆蓋細節信息,目錄試圖、文件試圖和源代碼試圖,允許快速瀏覽代碼覆蓋率數據。
2.2 原理簡介
2.2.1 概念解釋
下面對覆蓋率技術的常見概念進行簡單介紹。主要是基本塊(Basic Block),基本塊圖(Basic Block Graph),行覆蓋率(line ), 分支覆蓋率( )等。
基本塊(Basic Block),"A basic block is of with only entry and only one exit. If any one of are , they will all be , and in to last." 這里可以把基本塊看成一行整體的代碼,基本塊內的代碼是線性的,要不全部運行,要不都不運行。
基本塊圖(Basic Block Graph),基本塊的最后一條語句一般都要跳轉,否則后面一條語句也會被計算為基本塊的一部分。如果跳轉語句是有條件的,就產生了一個分支(arc),該基本塊就有兩個基本塊作為目的地。如果把每個基本塊當作一個節點,那么一個函數中的所有基本塊就構成了一個有向圖,稱之為基本塊圖(Basic Block Graph)。且只要知道圖中部分BB或arc的執行次數就可以推算出所有的BB和所有的arc的執行次數。
圖1 基本塊圖
打樁,意思是在有效的基本塊之間增加計數器,計算該基本塊被運行的次數;打樁的位置都是在基本塊圖的有效邊上。
行覆蓋率(line ),源代碼有效行數與被執行的代碼行的比率。
分支覆蓋率( ),有判定語句的地方都會出現2個分支,整個程序經過的分支與所有分支的比率是分支覆蓋率。注意,與條件覆蓋率( )有細微差別,條件覆蓋率在判定語句的組合上有更細的劃分。 2.2.2 編譯選項
gcc需要靜態注入目標程序編譯選項,在編譯鏈接的時候加入2個選項(-ftest- --arcs ),編譯結束之后會生成 gcno文件,而經過靜態注入的目標程序在“正常結束”后,會在運行目錄下產生gcda數據文件,通過gcov工具就可產生覆蓋率數據結果。
-ftest-
讓編譯器生成與源代碼同名的gcno文件(note file),這種文件含有重建基本塊依賴圖和將源代碼關聯至基本塊及源代碼行號的必要信息。
--arcs
讓編譯器靜態注入對每個源代碼行關聯的計數器進行操作的代碼,并在鏈接階段鏈入經態度.a,其中包含在程序正常結束時生成gcda文件的邏輯和記錄弧跳變的次數及其他的概要信息。
在最終可執行程序進入用戶代碼入口函數之前調用 ()內部函數初始化統計數據區,并將()內部函數注冊為代碼出口。
當程序調用代碼出口正常結束時,()內部函數得到調用,其繼續調用()內部函數輸出統計數據并生成gcda文件,若程序是一個狀態機程序不會自動調用代碼出口時,需要增加定時器等方式調用()內部函數強制輸出gcda文件。
2.3 實踐應用
利用GCOV和LCOV工具可以進行嵌入式代碼覆蓋率的統計,需要在或者Scons文件中做下面的編譯鏈接設置,增加 --arcs -ftest- 或者 –,鏈接的時候,增加 --arcs 或者 –lgcov。
為了上述幾個編譯選項的使用不影響到正常的編譯過程和效率。可以使用中通過參數傳遞來支持覆蓋率產生,可以在使用下面的方式:
#代碼覆蓋率編譯選項
ifeq (_CODE_COV,$(CODE))
APP_FLAGS += -fprofile-arcs -ftest-coverage
endif
#代碼覆蓋率鏈接選項
ifeq (_CODE_COV,$(CODE))
LD_LINK_LIBFILTER += -fprofile-arcs -ftest-coverage
endif
這樣,可以使用 make CODE= 來引入這些編譯選項而不會影響到正常的編譯。
將目標機生成的gcda文件放回至編譯目錄下,利用LCOV命令“lcov –. – – file ”可以將gcno文件和gcda文件結合生成代碼覆蓋率結果info文件,再用LCOV命令“ –o html –title “LCOV–app.Info” –show- -”將info文件和源代碼文件結合轉化為可視化網頁形式。
圖2 LCOV生成HTML結果
3.高手總結方法
代碼覆蓋率等級
代碼覆蓋率可以通過多種方法測量。最常用的是測量以下一個或多個指標:語句覆蓋率,分支 覆蓋率,修訂的條件/判定覆蓋率(MC/DC)。以下章節中將逐一詳解這些代碼覆蓋率。
語句覆蓋率
語句覆蓋率用來度量被測代碼中的可執行語句是否被執行到,它并不考慮循環或者條件語句, 只針對語句度量可執行代碼。應當特別注意的是:“語句”并不等同于代碼行。
一般情況下,對于 C,C++,Java或Ada代碼覆蓋率統計工具,分號代表語句結束。在某些情況下,一條語句會跨越多行代碼。語句覆蓋率可以有效度量可執行代碼是否被執行,但同時也有一定的局限性。
語句覆蓋率的局限
考慮如下圖1的代碼段:
int* p = NULL;
if ()
p = &;
*p = 123;
圖 1 – 語句覆蓋局限代碼示例
如果“”為true,那么就有可能達到100%的語句覆蓋,然而這個測試用例忽略了另一種情況:如果“”為假代碼覆蓋率統計工具,程序將引用空指針,因此,雖然語句覆蓋率是一個很好的度量指標,它仍舊是入門級的代碼覆蓋率。理想情況下,即使“”為false,測試用例也應當被計算。
分支覆蓋率
分支覆蓋率用來度量程序中所有的判定和分支以及相應的輸出是否都被測試執行到,例如 “if”語句必須將“true”和“false”都考慮到以覆蓋所有的輸出。如果只有一個路徑被執行,那么覆蓋率將被標記為部分執行。
和語句覆蓋率類似,分支覆蓋浪費也有一些需要注意的細節,尤其在針對“惰性求值”的編程語言時,惰性求值是將代碼的求值操作延遲到需要結果值時再進行的一項技術。
分支覆蓋率的局限
典型的情況是當有復雜的布爾表達式的“惰性求值”出現時,如下圖2的代碼片段:
int* p = NULL;
if (condition1 && (condition2 || function1(*p)))
statement1;
else
考慮“”為假的情況,惰性求值將不會度量“”或,此種情況同樣會導致代碼“if ( && ( || (*p)))”的分支覆蓋率計算錯誤。
繼續考慮“”和“”都為真的情況。惰性求值將再次導致“(*p)” 不會被度量,也同樣會導致代碼“if ( && ( || (*p)))”的分支覆蓋率計算錯誤。在此種情況下,有可能出現分支覆蓋率為100%但軟件中仍有潛在缺陷的情況。
修訂條件/判定覆蓋率(MC/DC)
MC/DC是一種特殊的分支覆蓋率,它不但會使用分支覆蓋率報告復雜條件下的true和false輸出,同時也會報告復雜條件下的全部分支條件輸出。
MC/DC最初由波音公司創建,用于航空軟件中DO-178B的A級認證。通過對所有的子條件輸出分支的獨立證明,有效解決了惰性求值帶來的問題。
繼續討論代碼示例2,我們需要在“”和“(*p)”固定的條件下驗證“” 的“true”和“false”判定分支,之后繼續固定“”和“(*p)”驗證“” 的判定分支。
同樣的,讓我們在固定“”和“”的條件下討論 “(*p)”。在其他分支條件固定的情況下驗證某個分支條件的“true”和“false”值稱作“MC/DC對”。MC/DC對一般 使用MC/DC真值表描述。表1就是一個MC/DC真值表示例。
在軟件開發的不同階段獲取覆蓋率
軟件測試有很多種類,本文將其簡要的分為三類:
> 系統/函數測試:測試集成后的整個應用
> 集成測試:測試集成的子系統
> 單元測試:測試一個或多個文件或類
每個軟件項目在系統測試的過程中都會模擬最終用戶的操作對源代碼做一些系統測試。導致軟件發布后仍舊存在缺陷最重要的一個原因通常是程序在運行過程中遇到了非預期的,即沒有測試的輸入組合。
很多軟件項目并不是沒有做集成測試或者單元測試。只是在完成集成測試或單元測試后,開發團隊可能苦于為隔離程序中的單個或多個文必須所需的大量測試代碼量。
對于最嚴格的單元測試和集成測試來說,最終生成的測試代碼量比待測代碼量還要龐大是很經常出現的情況。因此,這兩種級別的測試普遍適用于關鍵和高安全領域,例如:航空航天、醫療、交通運輸、工業過程控制、高速汽車等。此類軟件中包含大量的嵌入式應用軟件。
關鍵領域的結構化測試流程一般會將需求的級別高低作為重點,代碼覆蓋率因而會在這種“基于需求”的測試中進行分析。在許多項目中,高等級的需求最先被測試。此時代碼覆蓋率可以被用來檢測和報告所達到的覆蓋比例。
然而不幸的是,在系統測試和功能測試階段想要達到100%的代碼覆蓋率幾乎是不可能的。通常情況下系統測試和功能測試只能達到60%-70%的代碼覆蓋率,剩余30%-40%的代碼覆蓋率需要在單元測試和集成測試階段才能夠完成。
單元測試使用包含驅動和樁的測試代碼隔離系統中的特定函數,同時使用測試用例模擬這些函數的執行。這些所謂的“低等級測試需求” 對被測試代碼提供了更高的控制,可以提高先前執行的系統測試覆蓋率(甚至能達到100%)。因此,在不同的測試之間共享覆蓋率數據是非常有必要的。
嵌入式環境中獲取覆蓋率帶來的挑戰
常言道“有得必有失”,在嵌入式環境獲取代碼覆蓋率的問題上,要付出的代價是對待測代碼額外的插樁工作。插樁是將額外的代碼添加到程序中,從而實現測試過程中的覆蓋率收集和分析操作。
由于插樁的相關操作將導致程序源代碼增多,進而延長程序的執行時間,因而需要預測插樁后的源代碼的覆蓋范圍預測,尤其當測試實時嵌入式系統環境時,此項工作就更為重要。
事實上,要精準的預測程序文件插樁的影響幾乎是不可能的。沒有算法支持(也不可能有)。每個系統都包含很多的變量,具有獨立唯一的復雜性。當然,對于典型的示例系統來說,獲取一組準確的估計還是可能實現的。
在共享環境中獲取覆蓋率數據
在嵌入式環境下管理代碼覆蓋率的主要問題在于如何配置內存以容納額外的插樁代碼。針對大量示例代碼評估后發現添加了上文中提出的各種覆蓋率額外配置之后,源代碼量增長量普遍達到了10%。對于絕大多數的32位目標板,這并不是一個很大的問題,但對于存儲容量有限的8位或者16位目標板來說,幾乎可以肯定這會是一個問題。
為了降低可執行文件的大小,各種各樣的代碼插樁技術被發明出來,針對不同大小的存儲區域有不同的數據采集技術。植入存儲器內部的收集系統可以用于監測被檢測到的代碼。這是插樁技術中保證使用最少RAM的關鍵技術。
4. 結語
通過以上的方法,可以統計C語言嵌入式代碼覆蓋率,統計結果為提高代碼質量提供了有效的依據,也為衡量測試質量提供了重要的指標,并可以通過結果中的代碼行執行次數進行效率分析。
然而,代碼覆蓋率并不能保證執行過的代碼質量,也無法作為衡量生產力的指標,代碼覆蓋率的數據只能表明測試用例的覆蓋代碼的強度,只有保證測試用例的正確通過和較高的代碼覆蓋率相結合才能真正意義上提升代碼的質量。
代碼覆蓋率能不能提高軟件的可靠性?答案是肯定的,代碼的覆蓋率分析是保證軟件質量最簡便易行的方法。
END