羅峰 徐金鵬
(同濟(jì)大學(xué),上海 201804)
主題詞:?jiǎn)卧獪y(cè)試 時(shí)鐘同步協(xié)議 開源單元測(cè)試框架 軟件質(zhì)量
隨著微控制器技術(shù)的發(fā)展,軟件開發(fā)在車載應(yīng)用中的重要性逐漸提高[1]。基于控制器的功能和安全等級(jí)等要求,不同控制器的軟件規(guī)模、開發(fā)難度和測(cè)試方法存在較大差異。傳統(tǒng)嵌入式軟件質(zhì)量受開發(fā)人員水平制約,其可靠性難以驗(yàn)證。同時(shí),受開發(fā)人員的流動(dòng)性影響,軟件在交接過程中可能引入更多漏洞,從而不能滿足要求或有缺陷[2]。為了解決以上問題,汽車開放系統(tǒng)架構(gòu)(AUTOmotive Open System Architecture,AUTO?SAR)[3]組織提出了開放的軟件架構(gòu),汽車工業(yè)軟件可靠性聯(lián)會(huì)(the Motor Industry Software Reliability Associa?tion,MIRSA)和HIS(Hersteller Initiative Software)等組織提出的規(guī)范提供了更嚴(yán)格的代碼檢查規(guī)則。開發(fā)者可以采用商用軟件進(jìn)行代碼生成、檢查和測(cè)試,從而減少或避免開發(fā)過程中軟件帶來的問題。
商用軟件既帶來了開發(fā)上的優(yōu)勢(shì),也帶來了成本上的增加,因此難以適用于按鍵、開關(guān)等低成本控制器。由于整車廠的設(shè)計(jì)要求也可能存在變化,因此部分零部件制造商依然采用傳統(tǒng)嵌入式軟件開發(fā)方法以便于靈活修改軟件。為了保證零部件質(zhì)量,整車廠針對(duì)零部件進(jìn)行測(cè)試并反饋存在的問題。由于制造商在軟件修改時(shí)可能引入新的漏洞,因此增加了整車廠的測(cè)試成本。同時(shí)由于整車廠測(cè)試環(huán)境與制造商開發(fā)環(huán)境不同,開發(fā)人員可能難以復(fù)現(xiàn)存在的問題,從而帶來軟件維護(hù)的困難。
為了減少以上問題,本文引入開源單元測(cè)試框架Cpputest[4-5],并以時(shí)間同步協(xié)議代碼為例提出針對(duì)車載控制器軟件的開發(fā)方法。通過對(duì)軟件模塊引入特定的測(cè)試用例,可以有效規(guī)范軟件模塊接口、減少缺陷出現(xiàn)幾率并提高多個(gè)開發(fā)者之間合作與交接的效率,從而催生出設(shè)計(jì)優(yōu)良和結(jié)構(gòu)緊湊的代碼。盡管測(cè)試用例的編寫增加了開發(fā)階段工作量,該方法提高了軟件質(zhì)量,減少了后期維護(hù)難度,從而降低了軟件實(shí)際開發(fā)成本。
嵌入式開發(fā)過程中底層與應(yīng)用程序邊界不清晰甚至混編導(dǎo)致軟件和硬件設(shè)計(jì)過程需相互協(xié)調(diào)和反饋。此時(shí)軟件功能與硬件緊密相連,因此缺乏繼承性并需隨著硬件升級(jí)重新開發(fā),開發(fā)完成后還需整體聯(lián)調(diào)才能確認(rèn)功能是否滿足要求[6]。如圖1a所示,常規(guī)開發(fā)流程中,開發(fā)人員參考設(shè)計(jì)需求和接口開展代碼編寫工作,受硬件功能和數(shù)量的限制,代碼中存在部分問題難以在調(diào)試和測(cè)試過程發(fā)現(xiàn),從而造成交付風(fēng)險(xiǎn)。交付后的漏洞通常采用打補(bǔ)丁的方法修復(fù),此時(shí)設(shè)計(jì)人員必須在有限的時(shí)間內(nèi)修改代碼并搭建測(cè)試環(huán)境進(jìn)行完整測(cè)試,而人工測(cè)試存在回歸性、效率、覆蓋率、數(shù)據(jù)的重用性等問題[7],因此修改過程中又可能引入新的漏洞,造成后期維護(hù)難度與成本增加。
圖1 開發(fā)流程對(duì)比
與常規(guī)開發(fā)流程相比,基于單元測(cè)試框架的代碼編寫過程中增加了測(cè)試用例的開發(fā)內(nèi)容,如圖1b所示,該開發(fā)流程中測(cè)試用例的制定總是執(zhí)行在代碼編寫之前,代碼更新和重構(gòu)后均可通過測(cè)試,從而保障輸出代碼總是滿足預(yù)定義的邏輯要求。測(cè)試用例的運(yùn)行借助了編程語言的特性。嵌入式代碼采用C語言編寫,由于C語言是跨平臺(tái)的,軟件模塊可以在主機(jī)上編譯運(yùn)行。通過特定參數(shù)輸入,觀察模塊輸出行為,可以測(cè)試軟件行為是否達(dá)到期望要求。由于測(cè)試在主機(jī)平臺(tái)上運(yùn)行,開發(fā)前期不需要硬件介入,因此可以有效減少與其他開發(fā)人員的資源矛盾[8]。
Cpputest是基于C++的測(cè)試框架,可以運(yùn)行在Visual Studio、eclipse和MinGW等環(huán)境中,被測(cè)模塊作為C代碼模塊鏈接到C++工程。如圖2所示,被測(cè)示例代碼的目的是將add的值加到p指針?biāo)赶虻臄?shù)據(jù)中。其開發(fā)過程為:
a.為了使整個(gè)C++工程完成編譯,將被測(cè)函數(shù)作為空函數(shù)加入工程中。
b.編寫并執(zhí)行測(cè)試用例1,由于空函數(shù)并不對(duì)指針p進(jìn)行操作,因此通過測(cè)試。
c.編寫測(cè)試用例2并執(zhí)行測(cè)試,由于空函數(shù)中p指向的數(shù)據(jù)沒有更新,測(cè)試失敗,此時(shí)程序顯示如圖3所示,測(cè)試結(jié)果顯示當(dāng)前2號(hào)用例第33行判斷出錯(cuò)。
d.添加賦值代碼,再運(yùn)行測(cè)試,測(cè)試用例2通過而測(cè)試用例1由于存在空指針訪問失敗。
e.被測(cè)代碼增加空指針保護(hù),并重新運(yùn)行測(cè)試用例,通過測(cè)試。
f.繼續(xù)添加測(cè)試用例和修改被測(cè)代碼。
圖2 單元測(cè)試示例
圖3 空函數(shù)運(yùn)行結(jié)果
被測(cè)代碼在每一次修改后均可快速執(zhí)行所有測(cè)試用例,因此開發(fā)者可以立即發(fā)現(xiàn)新的代碼帶來的漏洞并及時(shí)修改,從而避免了函數(shù)復(fù)雜度提高后調(diào)試帶來的額外工作量。由于測(cè)試用例在主機(jī)上運(yùn)行,因此測(cè)試具有以下優(yōu)點(diǎn):
a.速度快。如圖3所示,兩條測(cè)試用例的運(yùn)行時(shí)間小于1 ms,測(cè)試過程由CPU執(zhí)行,其速度遠(yuǎn)高于人工手動(dòng)測(cè)試。
b.可重復(fù)性高。每一次代碼修改后均可完整運(yùn)行所有測(cè)試用例,不會(huì)出現(xiàn)人為失誤導(dǎo)致的錯(cuò)誤結(jié)果。
c.無須硬件支持。當(dāng)實(shí)際硬件數(shù)量較少或者調(diào)試環(huán)境復(fù)雜時(shí),單元測(cè)試可以有效減少實(shí)際調(diào)試時(shí)間,從而降低調(diào)試成本。
d.測(cè)試更全面。對(duì)于部分偶發(fā)性或者由于器件老化帶來的故障(例如FLASH驅(qū)動(dòng)寫入數(shù)據(jù)失?。瑢?shí)物測(cè)試很難達(dá)到實(shí)際故障條件,而軟件可以模擬任意故障。
盡管Cpputest提供了良好的開發(fā)框架,實(shí)際開發(fā)過程中依然存在例如文件依賴、接口定義、寄存器訪問和中斷處理等問題。這些問題可能導(dǎo)致代碼處于不可測(cè)狀態(tài),從而使得基于單元測(cè)試的開發(fā)工作無法進(jìn)行。本節(jié)以車載時(shí)鐘同步協(xié)議軟件模塊為例,描述解決方法。
在主機(jī)上運(yùn)行代碼時(shí),首要條件即是編譯成功,這要求被測(cè)代碼支持在嵌入式和主機(jī)端均能編譯通過。盡管C語言具有一致性,代碼文件的依賴性可能導(dǎo)致編譯失敗。如圖4a所示,大部分驅(qū)動(dòng)代碼會(huì)引用交叉編譯器提供的寄存器描述文件,而主機(jī)編譯器并不提供該文件,從而導(dǎo)致文件缺失。
圖4 文件依賴關(guān)系
單片機(jī)在制造過程中普遍將寄存器映射到部分內(nèi)存地址,因此,對(duì)于軟件代碼而言,內(nèi)存和寄存器并沒有差別。如圖4b所示,通過將描述硬件模塊寄存器的結(jié)構(gòu)體存入模塊私有頭文件中,再在模塊初始化時(shí)傳入寄存器基地址,可以有效減少對(duì)交叉編譯器的依賴。同時(shí),在單元測(cè)試過程中,測(cè)試代碼可以引用該私有頭文件,即可任意操作寄存器行為,從而模擬實(shí)際工作條件下難以出現(xiàn)的故障。
除編譯器依賴外,軟件模塊常調(diào)用其他組件,因此,測(cè)試時(shí)需依據(jù)測(cè)試用例需求使用或替換被依賴的組件。如圖5所示,被測(cè)時(shí)鐘同步模塊有多個(gè)依賴組件,而測(cè)試用例通過輸入帶有不同時(shí)間戳的報(bào)文,查看同步模塊是否通過正確參數(shù)將本地時(shí)鐘頻率和相位的誤差傳遞給鐘修正模塊。為了截取函數(shù)調(diào)用參數(shù),采用仿冒的方法替換部分實(shí)際模塊,從而可以在每條測(cè)試用例中檢查實(shí)際時(shí)鐘修正值。
圖5 同時(shí)使用實(shí)際和仿冒模塊
通過合理的依賴關(guān)系設(shè)計(jì),被測(cè)代碼可以盡可能獨(dú)立于其他模塊,從而增加代碼可測(cè)性并減少自動(dòng)化測(cè)試成本。這將花費(fèi)設(shè)計(jì)人員更多時(shí)間,但可以使得依賴關(guān)系更加明確并增加代碼可復(fù)用性。同時(shí)由于被測(cè)代碼可以獨(dú)立編譯,因此更容易采用PC-Lint等工具執(zhí)行靜態(tài)檢查。
在引入單元測(cè)試框架后,代碼接口定義的合理性變得更加重要。如果被測(cè)模塊所有函數(shù)均沒有參數(shù),則該模塊可測(cè)性降低,而代碼編寫過程中部分函數(shù)名稱、參數(shù)、返回值和功能定義發(fā)生變化,則需修改所有與該部分相關(guān)的測(cè)試用例,從而導(dǎo)致工作量增加。
被測(cè)時(shí)鐘同步模塊的接口定義如表1所示,其中,接口分別與主程序、底層報(bào)文收發(fā)驅(qū)動(dòng)和時(shí)鐘模塊交互,從而達(dá)到校準(zhǔn)本地時(shí)鐘的目的。盡管全局變量也可用于模塊間交互并進(jìn)行單元測(cè)試,但實(shí)際使用中導(dǎo)致程序的狀態(tài)不可預(yù)測(cè),因此,時(shí)鐘同步的接口均為函數(shù)或函數(shù)指針,防止模塊變量被意外訪問。
表1 時(shí)間同步模塊函數(shù)接口
函數(shù)指針的另一個(gè)優(yōu)勢(shì)在于模塊內(nèi)部無需引用固定頭文件。例如,不同硬件平臺(tái)有不同的本地時(shí)鐘模塊,其頭文件名稱、函數(shù)定義均存在區(qū)別,如果直接引用則會(huì)出現(xiàn)文件依賴現(xiàn)象,導(dǎo)致移植時(shí)出現(xiàn)困難。函數(shù)指針可以有效減少依賴關(guān)系、降低耦合度并使接口與實(shí)現(xiàn)分開,因此更適合需要單元測(cè)試的代碼模塊。
完成接口設(shè)計(jì)后,首先制定測(cè)試用例。測(cè)試用例的具體內(nèi)容與函數(shù)定義、模塊功能需求有關(guān),一般涉及接口、局部數(shù)據(jù)結(jié)構(gòu)、邊界條件、獨(dú)立路徑和錯(cuò)誤處理路徑等5類[9]。通過總結(jié)時(shí)間同步模塊的測(cè)試用例,可以得到如表2所示的用例類型。其中,測(cè)試對(duì)象可以是單個(gè)函數(shù)或整個(gè)模塊,通過正確或錯(cuò)誤的調(diào)用檢查模塊行為是否滿足預(yù)期。由于每一次代碼改動(dòng)均會(huì)運(yùn)行所有用例,因此可以保障模塊行為總是滿足預(yù)期要求。
表2 測(cè)試用例類型
盡管測(cè)試用例可以有效約束模塊行為,但應(yīng)避免濫用。例如,測(cè)試過程中可以通過自定義函數(shù)返回模塊內(nèi)部變量進(jìn)一步監(jiān)視模塊狀態(tài)。然而代碼迭代開發(fā)過程中,內(nèi)部狀態(tài)邏輯可能發(fā)生變化,因此該測(cè)試用例反而限制了代碼的進(jìn)一步開發(fā)。通常,模塊對(duì)外接口不變,而模塊使用者也只關(guān)心接口,因此,測(cè)試用例應(yīng)當(dāng)類似于黑盒測(cè)試,其測(cè)試對(duì)象為實(shí)際使用中涉及到的真實(shí)接口,從而避免測(cè)試對(duì)開發(fā)的約束。良好的測(cè)試用例易于發(fā)現(xiàn)程序的錯(cuò)誤和缺陷,也易于實(shí)現(xiàn)代碼測(cè)試的完全覆蓋,因此其優(yōu)劣對(duì)軟件質(zhì)量的保證起著關(guān)鍵作用[10]。
盡管單元測(cè)試為代碼帶來好處,其工作模式依然存在問題,例如平臺(tái)差異和中斷處理。前者可以通過更具通用性的編碼方式解決,而后者則需要開發(fā)人員人工判斷。
3.4.1 編譯平臺(tái)差異
單元測(cè)試運(yùn)行在主機(jī)環(huán)境中,因此,其編譯平臺(tái)與嵌入式工作平臺(tái)存在差別,例如支持的語言特性、基本數(shù)據(jù)類型大小、字節(jié)序、數(shù)據(jù)對(duì)齊和中斷標(biāo)志位處理等,因此可能導(dǎo)致測(cè)試通過的代碼在實(shí)際運(yùn)行中出現(xiàn)問題。然而,該問題也說明了代碼通用性不足,模塊在移植到其它平臺(tái)時(shí)也可能遇到。因此,在初次實(shí)際調(diào)試中需注意平臺(tái)相關(guān)問題并盡可能解決,例如:對(duì)于數(shù)據(jù)類型長度,可以采用固定長度類型;對(duì)于大小端模式,可以加入宏定義判斷等。盡管代碼工作效率和內(nèi)存利用率可能降低,但修改后的代碼更適于運(yùn)行在多個(gè)平臺(tái)上,從而提高代碼可移植性,延長使用壽命。
3.4.2 無法覆蓋的用例
盡管測(cè)試用例可以測(cè)試正常和故障情況下模塊的行為,它并不能解決所有問題。最常見的不可測(cè)用例來源于中斷或者嵌入式操作系統(tǒng)任務(wù)調(diào)度。以表1中的函數(shù)為例,底層收發(fā)模塊調(diào)用P8021AS_rx_frame函數(shù)匯報(bào)收到的報(bào)文,而主函數(shù)周期調(diào)用P8021AS_tick函數(shù)處理收到的報(bào)文。兩個(gè)函數(shù)均會(huì)對(duì)接收緩沖區(qū)進(jìn)行操作,如果兩者運(yùn)行在不同優(yōu)先級(jí),則可能出現(xiàn)搶占行為,從而導(dǎo)致函數(shù)間公有的變量工作不正常,造成報(bào)文丟失等偶發(fā)故障。在模塊設(shè)計(jì)過程中并不能預(yù)測(cè)實(shí)際使用情況,因此圖5中引入隊(duì)列管理模塊,通過受到保護(hù)的先入先出隊(duì)列保證兩個(gè)函數(shù)之間不會(huì)出現(xiàn)搶占問題。除調(diào)度搶占外,對(duì)于同一函數(shù),由于操作系統(tǒng)調(diào)度也可能出現(xiàn)函數(shù)重入現(xiàn)象,如果代碼不可重入,則會(huì)導(dǎo)致工作異常。
Cpputest并不支持測(cè)試中斷搶占,因此測(cè)試用例運(yùn)行時(shí)并不會(huì)出現(xiàn)函數(shù)中斷和重入問題,而實(shí)際運(yùn)行過程中該問題出現(xiàn)的時(shí)機(jī)可能是隨機(jī)的,從而進(jìn)一步增加了調(diào)試難度。設(shè)計(jì)人員必須在設(shè)計(jì)階段減少模塊內(nèi)全局變量,分析每一個(gè)全局變量是否存在搶占的風(fēng)險(xiǎn),并對(duì)存在重入風(fēng)險(xiǎn)的代碼進(jìn)行保護(hù)以避免不可測(cè)問題帶來的影響。
時(shí)鐘同步模塊完成后單元測(cè)試結(jié)果如圖6所示,整個(gè)工程存在41條測(cè)試用例,而執(zhí)行一次的時(shí)間為2.29 s。整個(gè)執(zhí)行過程中有1 000 842次邏輯判斷,因此手動(dòng)測(cè)試難以覆蓋所有的測(cè)試需求。
由于測(cè)試時(shí)間較短,因此每一次修改完成后均可執(zhí)行所有測(cè)試用例。如圖7所示,假設(shè)代碼修改過程中開發(fā)人員偶然引入漏洞,將報(bào)文格式不正確時(shí)返回值A(chǔ)S_NOK改為了AS_OK,運(yùn)行時(shí)可以立即發(fā)現(xiàn)該漏洞并在輸出報(bào)告中指出失敗測(cè)試用例以及實(shí)際判斷代碼地址。由于有2條測(cè)試用例均會(huì)檢查該返回值,因此兩者均測(cè)試失敗。通過分析代碼、單步調(diào)試等手段可以很快定位到錯(cuò)誤點(diǎn)并及時(shí)修正。隨著被測(cè)模塊代碼量的增長,開發(fā)人員可能在被測(cè)模塊中加入代碼后卻沒有在任何測(cè)試用例中執(zhí)行。此時(shí),所有測(cè)試依然可以通過但未執(zhí)行代碼可靠性存在風(fēng)險(xiǎn)。此時(shí)可借助OpenCppCoverage等第三方覆蓋率檢查工具查看被測(cè)模塊在整個(gè)測(cè)試過程中沒有執(zhí)行的代碼。如果存在此類代碼,則可以通過刪除代碼或者增加測(cè)試用例的方式修改開發(fā)工程,從而保障單元測(cè)試的完整性。
圖6 時(shí)間同步模塊運(yùn)行測(cè)試
圖7 錯(cuò)誤修改后立即報(bào)錯(cuò)
本文為車載嵌入式控制器軟件開發(fā)引入開源單元測(cè)試框架Cpputest,從而使模塊開發(fā)過程與代碼單元測(cè)試相結(jié)合。通過合理的文件依賴關(guān)系和可測(cè)的代碼接口,軟件模塊可以脫離實(shí)際硬件平臺(tái)運(yùn)行。在測(cè)試用例的幫助下,每一次代碼改動(dòng)均可完整驗(yàn)證其可靠性,從而避免漏洞引入。實(shí)際硬件調(diào)試過程中遇到的問題也可轉(zhuǎn)化為測(cè)試用例,從而避免已經(jīng)修改的漏洞反復(fù)出現(xiàn)并提高了代碼質(zhì)量。盡管開發(fā)過程中初期工作量更大,該方法強(qiáng)制開發(fā)者使用更合理的軟件架構(gòu),減少了后期維護(hù)難度與成本。而測(cè)試用例亦可用于形成文檔化的軟件說明,從而減少模塊的交接、移植等工作帶來的影響,延長了軟件壽命。