2012-03-09 35 views
4

我正在編寫一個讀取二進制文件格式的C庫。我不控制二進制格式;它是由專有的數據採集程序生成的,並且相對複雜。由於這是我第一次嘗試C編程和二進制文件解析,所以我在確定如何構造用於測試和可移植性的代碼時遇到了一些問題。單元測試C中的二進制格式閱讀器

出於測試目的,我認爲最簡單的方法是構建庫來讀取任意字節流。但我最終實現了一個數據類型,封裝了流的類型(memstream,filestream等)。該接口具有如stream_read_uint8這樣的功能,使得客戶端代碼不必知道字節來自何處。我的測試是針對memstream,而filestream東西基本上就在FILE*fread包裝等

從一個面向對象的角度來看,我覺得是一個合理的設計。但是,我感覺到我正在將錯誤的範例塞進語言中,並最終導致過度抽象,過於複雜的代碼。

所以我的問題:是否有一個更簡單,更習慣的方式來做二進制格式閱讀純C同時保留自動化測試?

注:我知道FILE*本質上是一個抽象流接口。但是內存流的實現(fmemopen)是非標準的,我希望標準C具有可移植性。

+1

在我的專家看來,我認爲只要你的封裝使你的用戶可以使用和/或解決一個問題,並且代碼是合理可維護和直接的,你可能是沒問題的。 – prelic 2012-03-09 23:34:22

+0

@prelic,謝謝,當然這是有道理的。我只是將這個項目作爲一個學習練習,所以我對C最佳實踐和規範感興趣。 – yamad 2012-03-09 23:44:01

+1

我參與過讀取二進制格式的項目,這是我們使用過的方法,並且被預先存在的程序使用 - 一次讀取X位,讀取後在緩衝區中標記佔位符,並將數據返回爲uint8_t *,讓調用者轉換成任何需要的類型。不是你的問題的答案,只是我的經驗。 – greg 2012-03-09 23:45:37

回答

2

您所描述的是低級I/O功能。由於fmemopen()不是100%可移植的(關閉Linux,它會吱吱嘎嘎,我懷疑),那麼您需要爲自己提供一些可移植的東西,這些東西足夠接近,以便在必要時使用代理函數(僅),並使用native儘可能發揮功能。當然,即使在您的本地棲息地,您也應該能夠強制使用您的功能,以便測試您的代碼。

這段代碼可以用已知數據進行測試,以確保您獲取輸入流中的所有字符,並可以忠實地返回它們。如果原始數據具有特定的字節順序,則可以確保假設您的「較大」類型的函數(如stream_read-uint2(),stream_read_uint4(),stream_read_string(),stream_read_string()等—)的行爲都適當。在這個階段,你並不需要真正的數據。您可以製作適合自己和您的測試的數據。

一旦掌握了這些內容,您還需要編寫代碼來讀取數據類型較大的數據,並確保這些更高級別的函數實際上可以準確解釋二進制數據並調用適當的操作。爲此,您最後需要格式提供的示例;直到這個階段,你可能會擺脫你製造的數據。但是,一旦你正在閱讀實際的文件,你需要那些工作的例子。或者你必須儘可能地從理解和測試中製造它們。這很容易取決於二進制格式的清晰記錄。


其中一個關鍵的測試和調試工具將是規範的「轉儲」功能,可以爲您呈現數據。我使用的方案是:

extern void dump_XyzType(FILE *fp, const char *tag, const XyzType *data); 

該流是不言而喻的;通常是stderr,但通過將其作爲參數,可以將數據獲取到任何打開的文件。打印的信息中包含tag;它應該是唯一的以識別呼叫的位置。最後一個參數是一個指向數據類型的指針。你可以分析和打印。您應該藉此機會斷言您可以想到的所有有效性檢查,以解決問題。

您可以將接口擴展爲, const char *file, int line, const char *func,並安排將__FILE____LINE____func__添加到調用中。我從來沒有很需要它,但如果我這樣做,我會使用:

#define DUMP_XyzType(fp, tag, data) \ 
     dump_XyzType(fp, tag, data, __FILE__, __LINE__, __func__) 

舉個例子,我處理一個類型DATETIME,所以我有一個函數

extern void dump_datetime(FILE *fp, const char *tag, const ifx_dtime_t *dp); 

一個我用的是這個星期的測試可以說服轉儲日期時間價值,它給了:

DATETIME: Input value -- address 0x7FFF2F27CAF0 
Qualifier: 3594 -- type DATETIME YEAR TO SECOND 
DECIMAL: +20120913212219 -- address 0x7FFF2F27CAF2 
E: +7, S = 1 (+), N = 7, M = 20 12 09 13 21 22 19 

你可能會或可能無法看到有一個值2012-09-13 21:22:19。有趣的是,這個函數本身調用系列中的另一個函數dump_decimal()來打印出十進制值。有一年,我會升級限定符打印以包含十六進制版本,這很容易閱讀(3594是0x0E0A,這很容易被那些被稱爲14位數字(E)的人理解),從YEAR開始(第二個0)到second(A),這當然不是十進制版本中顯而易見的。當然,信息是字符串中的類型:DATETIME YEAR TO SECOND。(十進制格式對於局外人來說有點難以理解,但是非常清晰知道有指數(E),符號(S),數字(百位)數字(N = 7)和實際數字(M = ...)的內部人員。是的,名稱decimal是嚴格的一個用詞不當,因爲它使用了一個基本的100或百倍的表示)。

該測試默認情況下不會產生該詳細程度,但我只需運行一個足夠高級別的調試集(通過命令線選項),我會考慮這一點作爲另一個有用的功能

運行測試的最安靜的方式生產:

test.bigintcvasc.......PASS (phases: 4 of 4 run, 4 pass, 0 fail)(tests: 92 run, 89 pass, 3 fail, 3 expected failures) 
test.deccvasc..........PASS (phases: 4 of 4 run, 4 pass, 0 fail)(tests: 60 run, 60 pass, 0 fail) 
test.decround..........PASS (phases: 1 of 1 run, 1 pass, 0 fail)(tests: 89 run, 89 pass, 0 fail) 
test.dtcvasc...........PASS (phases: 25 of 25 run, 25 pass, 0 fail)(tests: 97 run, 97 pass, 0 fail) 
test.interval..........PASS (phases: 15 of 15 run, 15 pass, 0 fail)(tests: 178 run, 178 pass, 0 fail) 
test.intofmtasc........PASS (phases: 2 of 2 run, 2 pass, 0 fail)(tests: 12 run, 8 pass, 4 fail, 4 expected failures) 
test.rdtaddinv.........PASS (phases: 3 of 3 run, 3 pass, 0 fail)(tests: 69 run, 69 pass, 0 fail) 
test.rdtimestr.........PASS (phases: 1 of 1 run, 1 pass, 0 fail)(tests: 16 run, 16 pass, 0 fail) 
test.rdtsub............PASS (phases: 1 of 1 run, 1 pass, 0 fail)(tests: 19 run, 15 pass, 4 fail, 4 expected failures) 

每個程序識別本身和它的狀態(通過或失敗)和總結統計。我一直在尋找bug,並修復了一些我偶然發現的bug,所以有一些「預期的失敗」。這應該是一種暫時的事態;它允許我合法地聲稱測試全部通過。如果我想要更多的細節,我可以運行任何測試,任何階段(有些相關測試的子集,雖然'有點'實際上是任意的),並且看到全部結果等。如圖所示,運行該組測試需要不到一秒的時間。

我發現這有助於有重複計算的地方 - 但我必須計算或驗證每個測試在某個點上的正確答案。