1. 程式人生 > 其它 >用 shell 指令碼做自動化測試

用 shell 指令碼做自動化測試

一個檔案監控引擎的自動化測試指令碼,難倒我的不是如何編寫測試用例和校驗輸出,而是 msys2 的 stdin 重定向和 console 程式的行緩衝設定…

前言

專案中有一個功能,需要監控本地檔案系統的變更,例如檔案的增、刪、改名、檔案資料變動等等。之前只在 windows 上有實現,採用的是 iocp + ReadDirectoryChanges 方案,現在隨著整個應用移植到 mac 上,需要對這一部分進行適配,macOS 上相應的底層機制為 File System Events,通知的型別大同小異。為了便於驗證,寫了一個 demo 來跑最核心的功能。

macOS

開門見山,先來看在 mac 上的實現。

rdc-demo

這個 demo 是從 windows 遷移過來的,所以名稱中仍保留了 ReadDirectoryChanges (rdc) 的痕跡。它接收一個目錄路徑作為監聽目錄,在該目錄下的操作會觸發系統通知從而列印相關日誌。例如在監控目錄輸入下面的命令:

$ cd /var/tmp/rdc
$ touch a-file
$ echo abc > a-file
$ mv a-file b-file
$ rm b-file
$ mkdir a-dir
$ touch a-dir/c-file
$ mv a-dir b-dir
$ mv b-dir/c-file ./d-file
$ rmdir b-dir
$ mv d-file ../

會生成如下的 console 輸出:

$ ./rdc-demo /var/tmp/rdc
dir: /var/tmp/rdc
create worker thread 0x16f207000
start monitoring...
add file: /private/var/tmp/rdc/a-file
add file: /private/var/tmp/rdc/a-file
modify file: /private/var/tmp/rdc/a-file
remove file: /private/var/tmp/rdc/a-file
add file: /private/var/tmp/rdc/b-file
find removed flag with renamed path, ignore..
remove file: /private/var/tmp/rdc/b-file
add dir: /private/var/tmp/rdc/a-dir
add file: /private/var/tmp/rdc/a-dir/c-file
remove dir: /private/var/tmp/rdc/a-dir
add dir: /private/var/tmp/rdc/b-dir
remove file: /private/var/tmp/rdc/b-dir/c-file
add file: /private/var/tmp/rdc/d-file
remove dir: /private/var/tmp/rdc/b-dir
remove file: /private/var/tmp/rdc/d-file

get char 10, errno 2
stop run loop
loop exit
stopping event stream...
end monitor

使用者輸入任意字元 (例如回車) 可以讓 demo 退出監控。再來看監控目錄中輸入的命令,檔案、目錄的增刪改都有涉及,應該說比較全面了。隨著程式碼的不斷完善,為保證不引入 bug,每次都需要執行上面一長串操作並對比著輸出看,著實是一件費眼費力的事;隨著考慮的場景增多 (例如將檔案移到廢紙簍、從監控目錄外移入一個檔案、檔案大小寫、軟連結…),這個操作列表也在不斷增長,如何提高測試效率成為一個擺在眼前的實際問題。於是自然而然的想:能不能用 shell 指令碼自動化執行上述測試工作?通過執行一個指令碼就把上面一系列操作執行完並給出最終測試結論就好了,於是有了下面的探索過程。

後臺程序

一開始想法比較簡單,就是在一個指令碼中啟動 demo,同時在監控目錄中操作檔案或目錄,每個動作完成後,等待 demo 的輸出,如果檢測到對應的關鍵字 (例如 add / remove / rename / modify + file / dir),說明測試通過,否則測試不通過,最後列印通過與不通過的用例總數作為彙總。那麼如何獲取 demo 輸出的內容呢?最直觀的方案就是輸出重定向啦,這個可以在啟動 demo 時“做手腳”,因此先來看 demo 的啟動部分。

啟動 demo 和跑測試用例需要並行,因此有一個程序是執行在後臺的。直覺是將 demo 作為後臺程序更合適些,特別是將它的輸入輸出都重定向到 pipe 後,只要從 out-pipe 讀、從 in-pipe 寫就可以了,其中 out-pipe 是 demo 的 stdout 用來驗證 demo 列印的資訊;in-pipe 是 demo 的 stdin,用來在跑完所有用例後告訴 demo 該退出了,這個過程堪稱完美!於是有了下面的指令碼:

 1 #! /bin/sh
 2 
 3 #########################################3
 4 # main
 5 if [ -z "$RDC_HOME" ]; then 
 6     echo "must define RDC_HOME to indicate test program root dir before run this test script!"
 7     exit 1
 8 fi
 9 
10 echo "init fifo"
11 # need 2 fifo to do full duplex communication
12 pipe_name_out="$$.out.fifo"
13 pipe_name_in="$$.in.fifo"
14 mkfifo "$pipe_name_out"
15 mkfifo "$pipe_name_in"
16 # 6 - out
17 # 7 - in
18 exec 6<>"$pipe_name_out"
19 exec 7<>"$pipe_name_in"
20 
21 dir="tmp"
22 echo "init dir: $dir"
23 mkdir "$dir"
24 
25 echo "start work process"
26 # it works again, should be error on '<>&6'
27 # our in (6) is their out; our out (7) is their in..
28 "$RDC_HOME/rdc-demo" "$dir" >&6 2>&1 <&7 &
29 # should cd after start demo, otherwise rdc-demo will complain no tmp exist..."
30 cd "$dir"
31 
32 # TODO: add test cases here
33 
34 echo "press any key to exit..."
35 resp=""
36 read resp
37 
38 echo "notify work process to exit"
39 echo "1" >&7
40 
41 echo "start waiting work process"
42 wait
43 
44 echo "waited, close fifo"
45 exec 6<&-
46 exec 7<&-
47 
48 cd ..
49 echo "cleanup"
50 rm -rf "$dir"
51 rm "$pipe_name_in"
52 rm "$pipe_name_out"
53 echo "all done"

重點在 line 28,這裡簡單說明一下:

  • 為了遮蔽不同平臺的差異,需要定義一個 RDC_HOME 的環境變數指向 rdc-demo 所在的路徑;
  • 為了方便,監聽目錄就是測試指令碼同目錄的 tmp 資料夾,這是 main 指令碼臨時建立的,測試完成後會清理掉;
  • demo 輸出重定向到 xx.out.fifo 檔案,這裡出於便捷考慮使用控制代碼 6 代表; 輸入重定向到 xx.in.fifo,使用控制代碼 7 代表;其中 xx 為程序號,當重啟指令碼時可以防止和舊的程序相互干擾;控制代碼 6 和 7 就是隨便找的兩個數,大於 3 都可以;
  • 將標準錯誤 2 重定向到標準輸出 1,也就是 xx.out.fifo 檔案中,2>&1 這一句一定要放在重定向 >&6 之後,不然不會生效;
  • 最後以後臺程序 (&) 執行這個 demo

啟動完程序後就可以繼續執行了:

  • 在 line 32 處插入指令碼跑測試用例;
  • 當用例都跑完後,line 36 等待使用者輸入任意字元退出;
  • line 39 通過控制代碼 7 通知 demo 退出;
  • line 42 同步 demo 的退出狀態;
  • line 45-46 關閉 fifo 檔案;
  • line 50-52 清理臨時檔案和目錄。

抽取輸出

事情到這裡只做了一半,demo 的輸出都會存放在控制代碼 6 代表的 fifo 中,最好抽取出來展示一下:

 1 pump_output()
 2 {
 3     # read until 'start'
 4     local line
 5     local str
 6     while read line
 7     do
 8         str=${line/"start"//}
 9         if [ "$str" = "$line" ]; then 
10             # no change, continue read
11             echo "cmd skip: $line"
12         else
13             str=${line/"failed"//}
14             if [ "$str" = "$line" ]; then 
15                 # no change, success
16                 echo "cmd start ok: $line"
17                 return 0
18             else
19                 # fail
20                 echo "cmd start failed: $line"
21                 return 1
22             fi
23         fi
24     done <&6
25 }

回憶之前 rdc-demo 的輸出,在正式開始校驗前會有一些無關的輸出,這個 pump_output 正好能起到過濾的作用。下面簡單說明一下

  • line 24:這是這個 while 迴圈的關鍵,不斷從控制代碼 6 代表的 out-fifo 中讀取 demo 的輸出;
  • line 8:過濾 ”start monitoring failed“ 關鍵行,如果沒有就跳過;
  • line 17:定位到 start 關鍵行,如果沒有 failed 關鍵字認為初始化成功,結束抽取退出迴圈;
  • line 21:如果有 failed 關鍵字,初始化失敗,結束抽取退出程序;

把這個函式放在 main 的 line 32 之前:

 1 # pump out until known it start ok or fail
 2 pump_output
 3 if [ $? -eq 0 ]; then 
 4     # add test case here
 5 
 6     echo "press any key to exit..."
 7     resp=""
 8     read resp
 9 
10     echo "notify work process to exit"
11     echo "1" >&7
12 else
13     echo "start demo failed, exit.."
14 fi

上面的指令碼輸出如下:

$ sh rdc_main.sh
init fifo
init dir: tmp
start work process
cmd skip: dir: tmp
cmd skip: create worker thread 0x16fd6f000
cmd start ok: start monitoring...
press any key to exit...

notify work process to exit
start waiting work process
waited, close fifo
cleanup
all done

絲般順滑。

校驗輸出

下面再加點戲,測試一個最簡單的建立檔案的場景:

 1 # add test case here
 2 touch "a-file"
 3 read line <&6
 4 
 5 str=${line/'add file'//}
 6 if [ "$str" = "$line" ]; then 
 7     # no change, character string not find.
 8     echo "[FAIL]: $line"
 9 else
10     echo "[PASS]: $line"
11 fi

這個用例在監控目錄建立了一個檔案 (line 2),它期待從 out-pipe 中讀取包含 "add file xxx" 的輸出 (line 3),這裡使用了 shell 的字串刪除,如果刪除後字串與原串相同,說明沒有這個關鍵字,那麼驗證失敗,否則成功,不管是否成功,都列印原串告知使用者。下面是指令碼的輸出:

$ sh rdc_main.sh
init fifo
init dir: tmp
start work process
cmd skip: dir: tmp
cmd skip: create worker thread 0x16d753000
cmd start ok: start monitoring...
[PASS]: add file: /Users/yunhai01/test/rdc-test/tmp/a-file
press any key to exit...

notify work process to exit
start waiting work process
waited, close fifo
cleanup
all done

與預期完全一致。由於校驗輸出後面會經常用到,可以將它封裝成一個 function:

 1 # $1: expect
 2 # $2: output
 3 verify_output()
 4 {
 5     local expect="$1"
 6     local output="$2"
 7     local str=${output/"$expect"//}
 8     if [ "$str" = "$output" ]; then 
 9         # no change, character string not find.
10         echo "[FAIL]: $output"
11         return 1
12     else
13         echo "[PASS]: $output"
14         return 0
15     fi
16 }

使用時像下面這樣呼叫即可:

verify_output "add file" "$line"

引數一表示期望包含的關鍵字;引數二表示從 out-pipe 讀取的源串,後面會大量的使用到這個例程。

Windows

在開始編寫正式用例之前,讓我們再來看下 windows 平臺上的實現。由於不能直接在 windows 上執行 shell 指令碼,我使用了 msys2 環境,它基於 cygwin 和 mingw64,但更輕量,就是 git bash 使用的那一套東西啦~ 但畢竟是移植的,和原生的 unix shell 還有是差別的,所以這裡繞了一些彎路,下面是探索的過程。

rdc-demo.exe

windows 上也有 demo 程式,之前的操作序列對應的輸出如下:

$ ./rdc-demo tmp
dir: tmp
start monitoring...
add file: tmp\a-file
modify file: tmp\a-file
modify file: tmp\a-file
modify file: tmp\a-file
rename file from tmp\a-file to tmp\b-file
remove file: tmp\b-file
add dir: tmp\a-dir
add file: tmp\a-dir\c-file
modify file: tmp\a-dir\c-file
modify dir: tmp\a-dir
rename dir from tmp\a-dir to tmp\b-dir
remove file: tmp\b-dir\c-file
add file: tmp\d-file
modify dir: tmp\b-dir
remove file: tmp\b-dir
remove file: tmp\d-file

get char 10, errno 0
end monitor

可以看到和 mac 上還是有一些差異的,不過這裡先不展開,重點放在對 windows demo 輸出的大致瞭解上。

前臺程序

將上面 mac 相對完善的指令碼在 windows 下執行,得到了下面的輸出:

$ sh rdc_main.sh
init fifo
init dir: tmp
start work process
cmd skip: dir: tmp
cmd start ok: start monitoring...
[FAIL]: get char -1, errno 5
press any key to exit...

notify work process to exit
start waiting work process
waited, close fifo
cleanup
all done

沒有得到期待的 ”add file xxx“,而是直接出錯了,5 是錯誤碼 EIO:io error,而且是在 getchar 等待使用者輸入時報錯的,正常情況下這裡應該阻塞直到使用者輸入任意字元,這裡卻直接出錯,難道是因為我重定向了 stdin 到 pipe-in (7) 嗎?於是將這個重定向去掉,保持 stdin 為 console 不變:

"$RDC_HOME/rdc-demo" "$dir" >&6 2>&1 &

再執行指令碼,依然報錯:

$ sh rdc_main.sh
init fifo
init dir: tmp
start work process
cmd skip: dir: tmp
cmd start ok: start monitoring...
[FAIL]: get char -1, errno 6
press any key to exit...

notify work process to exit
start waiting work process
waited, close fifo
cleanup
all done

錯誤碼變了,6 為 ENXIO:no such device… 這回更丈二和尚摸不著頭腦了。第一反應是 getchar 的問題,於是將 demo 中的 getchar 分別替換為 getc(stdin)、 scanf 甚至 ReadConsole,但是都沒有改善。最後逼的我沒辦法,甚至把這塊改成了死迴圈,也沒有好,直接什麼輸出也沒有了!

整理一下思路,最後一種死迴圈顯然不可取,因為即使可行也會導致 demo 程序無法退出。因此還是聚焦在讀 stdin 失敗的問題上,我發現指令碼也有讀使用者輸入的場景,卻完全沒問題,難道是因為將 demo 啟動為了後臺程序的緣故?學過 《apue》程序關係這一節的人都知道,後臺程序是不能直接讀使用者輸入的,否則前後臺同時讀取使用者輸入會形成競爭問題從而造成混亂,為此 unix 會向嘗試讀 stdin 的後臺程序傳送 SIGTTIN 訊號,預設行為會掛起正在執行的後臺程序;同理,嘗試輸出到 stdout 的後臺程序也會收到 SIGTTOUT 訊號,預設行為在不同平臺上不同,linux 為掛起,mac 為忽略。回到前面的問題,windows 上本身沒有訊號,所以我猜測 msys2 只能讓嘗試讀取 stdin 的後臺程序出錯了事。

分析到這一步,嘗試讓 demo 執行在前臺,將跑測試用例的指令碼封裝在 do_test_case 的例程中執行在後臺。修改後的指令碼如下:

 1 dir="tmp"
 2 echo "init dir: $dir"
 3 mkdir "$dir"
 4 
 5 do_test_case "$dir" &
 6 
 7 echo "start work process"
 8 "$RDC_HOME/rdc-demo" "$dir" >&6 2>&1
 9 
10 echo "start waiting work process"
11 wait

為了保證後臺程序有機會執行,將它放在 rdemo 的啟動之前。下面是 do_test_case 的內容:

 1 # $1: dir
 2 do_test_case()
 3 {
 4     local dir="$1"
 5     echo "start pumping"
 6     # pump out until known it start ok or fail
 7     pump_output
 8     if [ $? -ne 0 ]; then
 9         echo "start demo failed, exit.."
10         return 1
11     fi
12 
13     # should cd after start demo, otherwise rdc-demo will complain no tmp exist..."
14     # sleep (1)
15     cd "$dir"
16 
17     # add test case here
18     touch "a-file"
19     read line <&6
20     verify_output "add file" "$line"
21 
22     echo "press any key to exit..."
23     # cd back
24     cd ..
25 }

與之前的幾點不同:

  • pump_output 從啟動 demo 之後移到了這裡,它執行完畢時可以確定 demo 已經輸出 start monitoring ok/failed;
  • 切換目錄由啟動 demo 之後移動了這裡,由於指令碼是在監控目錄中執行的,所以只要保證它包住測試用例即可;不用擔心切換目錄早於 demo 啟動從而導致後者找不到監控目錄,上面的 pump_output 保證了當執行到這裡時 demo 已經啟動並就緒,所以這裡不需要額外的 sleep (line 14);
  • test case 處仍從控制代碼 6 讀取 demo 輸出,這一點不變;
  • 不再等待使用者輸入,因為已處於後臺,是 main 中的 wait 在等待我們而不是相反,所以跑完所有用例後直接退出;
  • main 中也不再等待使用者輸入,因 demo 內部已有等待邏輯且執行在前臺,當用戶輸入任意字元後,首先導致 demo 退出,進而引發整個 main 退出。

與之前通順的指令碼相比,這次改的面目全非,ugly 了許多,但是為了能在 windows 上跑,顧不上什麼優雅不優雅了。執行起來後輸出如下:

$ sh rdc_main.sh
init fifo
init dir: tmp
start work process
start pumping

確實沒有再報讀取 stdin 的錯誤了,但是看樣子像卡住了,沒有輸出完整。

行緩衝

看上去卡死了,但是輸入回車後,又能接著跑:

$ sh rdc_main.sh
init fifo
init dir: tmp
start work process
start pumping

cmd skip: dir: tmp
cmd start ok: start monitoring...
start waiting work process
[FAIL]: get char 10, errno 0
press any key to exit...
waited, close fifo
cleanup
all done

從前面輸出的日誌看,應該是阻塞在啟動 pump_output (line 7),這極有可能是在讀取 out-fifo (6) 時卡住了。當輸入任意字元後,pump_output 又能從阻塞處返回並列印 demo 輸出,說明 demo 執行正常,而且使用者的輸入將 demo 從 getchar 喚醒並退出,所以後面的用例校驗沒有得到想要的結果,所以列印了 demo getchar 的結果。於是問題來了:為何一開始 pump_output 沒有抽取到輸出呢?

分析到這裡想必大家已經開始懷疑 demo 的輸出緩衝設定了,為了驗證這種懷疑,當時我用了一種笨辦法:在 demo 每行 printf 後面加了一句 fflush,結果輸出就正常了,可見確實是行緩衝失效所致。一般 console 程式的輸入輸出是行緩衝的,一旦進行檔案重定向後就會變成全緩衝,之前 pump_output 阻塞是因為系統認為資料還不夠,沒有讓 read 及時返回。找到了問題癥結,只需要在 demo 的 main 函式開始處加入以下兩行程式碼:

setvbuf (stdout, NULL, _IONBF, 0); 
setvbuf (stderr, NULL, _IONBF, 0);

就搞定了。明眼人可能看出來了,你這設定的是無緩衝啊,設定行緩衝不就行了麼?一開始我也是這樣做的:

setvbuf (stdout, NULL, _IOLBF, 0);
setvbuf (stderr, NULL, _IOLBF, 0);

結果還是沒有輸出,另外我還嘗試了以下形式:

// set size
setvbuf (stdout, NULL, _IOLBF, 1024);

// set buf & size
static char buf[1024] = { 0 }; 
setvbuf (stdout, buf, _IOLBF, 1024); 

都以失敗告終,就差使用 windows 上不存在的 setlinebuf 介面了,唉,windows 坑我!所以大家記住結論就好了:在 msys2 上將 console 程式重定向後,除非顯式在程式內部設定為無緩衝,否則一律為全緩衝。下面是正常的輸出:

$ sh rdc_main.sh
init fifo
init dir: tmp
start work process
start pumping
cmd skip: dir: tmp
cmd start ok: start monitoring...
[PASS]: add file: tmpa-file
press any key to exit...

start waiting work process
waited, close fifo
cleanup
all done

注意 msys2 將 win32 原生程式的路徑分隔符 ‘\’ 給吃掉了,這是因為 read 將它當作轉義符了,如果不想轉義,需要給 read 指定 -r 引數:

read -r line <&6 # -r: stop transform '\'

考慮到每個 verify_output 都需要這樣改,乾脆將這個 read 放在裡面完事:

 1 # $1: expect
 2 verify_output()
 3 {
 4     local expect="$1"
 5     local line=""
 6     read -r line <&6 # -r: stop transform '\'
 7     local str=${line/"$expect"//}
 8     if [ "$str" == "$line" ]; then 
 9         # no change, character string not find.
10         echo "[FAIL] $line" 
11         return 1
12     else
13         echo "[PASS] $line" 
14         return 0
15     fi
16 }

另外 demo 程式有一些警告和除錯輸出,不希望被用例抽取到,不然會導致測試用例失敗,為此將錯誤輸出重定向到 fifo 的 2>&1 語句去除,最終啟動 demo 的指令碼變為:

"$RDC_HOME/rdc-demo" "$dir" >&6 

這樣一改,setvbuf 也只需要作用於 stdout,demo 中加一行程式碼就可以了。

跨平臺

真實的場景中,我是先將 windows 上的現成程式碼做成小 demo 驗證的,指令碼也是先在 windows 上構建的,然後就遇到了讀 stdin 失敗和行緩衝的問題,折騰了很久才搞定,後來遷移到 mac 上時,這個過程反而沒遇到什麼問題——測試指令碼相對比較完善了,只補了一個 demo,調整了個別用例就能跑通了。在寫作本文的時候,好奇後臺程序方案在 mac 上的表現,於是重新在 mac 上驗證了下,結果沒什麼問題,看來還是原生的好呀。為了讓讀者由淺入深的理解這個過程,這裡調換了一下按時間線順序述說的方式,避免一上來就掉在 msys2 的坑裡產生額外的理解成本。

考慮到後臺程序方案只在 mac 上可行;而前臺程序在兩個平臺都可行,所以最後的方案選擇了前臺程序。

編寫用例

搞定了自動化測試指令碼框架,現在可以進入正題了。在前面已經展示過如何寫一個最簡單的用例——基本上就是操作檔案、驗證輸出這兩步,下面分別按檔案與目錄的型別進行說明。

檔案變更

檔案變更覆蓋了建立、寫入資料、追加資料、重新命名、刪除幾個場景,考慮到 mac 和 windows 輸出不同,這裡也分平臺構建,即將不同平臺的用例放置在不同的指令碼檔案中,在 main 指令碼執行時根據當前平臺載入並呼叫之:

 1 # $1: dir
 2 do_test_case()
 3 {
 4     local dir="$1"
 5     echo "start pumping"
 6     # pump out until known rdc-demo start ok or fail
 7     pump_output
 8     if [ $? -ne 0 ]; then 
 9         echo "pump failed" 
10         return 1
11     fi
12 
13     # do real test here
14     if [ $is_macos -ne 0 ]; then 
15         # start dirty work
16         source ./test_case_macos.sh
17     else
18         # windows
19         source ./test_case_windows.sh
20     fi
21 
22     # should cd after start demo, otherwise rdc-demo will complain no tmp exist..."
23     cd "$dir"
24     # add test case here
25     test_file_changes "test.txt"
26 
27     echo "press any key to exit..."
28     # cd back
29     cd ..
30 }

重點在 line 14-20,這裡測試用例分兩個檔案存放,mac 平臺放置在 test_case_macos.sh; windows 平臺放置在 test_case_windows.sh;以後有新的平臺 (例如 linux) 只需增加相應的平臺檔案和平臺判斷即可,有利於提升測試框架的拓展性。line 25 呼叫測試介面,這裡約定的介面名稱是 test_file_changes,它接收一個引數,是建立的測試檔名 (test.txt)。關於全域性變數 is_macos,是在 main 指令碼起始處初始化的:

is_macos=0
os="${OSTYPE/"darwin"//}"
if [ "$os" != "$OSTYPE" ]; then 
    # darwin: macos
    is_macos=1
fi

這裡直接使用即可,下面分平臺看下 test_file_changes 的實現。

macOS

 1 # $1: file name to monitor
 2 test_file_changes()
 3 {
 4   local file="$1"
 5   local newfile="new.$file"
 6 
 7   touch "$file"
 8   echo "touch $file :"
 9   verify_output "add file" 
10 
11   echo "first line" > "$file"
12   echo "modify $file :"
13   # '>' trigger 2 actions for newly created file
14   verify_output "add file" 
15   verify_output "modify file" 
16 
17   echo "last line" >> "$file"
18   echo "modify $file :" 
19   # '>>' triger 2 actions, too
20   verify_output "add file" 
21   verify_output "modify file"
22 
23   mv "$file" "$newfile"
24   echo "move $file $newfile :" 
25   # all rename on macOS is transfered to remove & add pair.
26   verify_output "remove file"
27   verify_output "add file"
28 
29   rm "$newfile"
30   echo "remove $file :" 
31   verify_output "remove file"
32 }

比較直觀。

Windows

 1 # $1: file name to monitor
 2 test_file_changes()
 3 {
 4   local file="$1"
 5   local newfile="new.$file"
 6 
 7   touch "$file"
 8   echo "touch $file :" 
 9   # touch trigger 2 actions
10   verify_output "add file" 
11   verify_output "modify file"
12 
13   echo "first line" > "$file"
14   echo "modify $file :" 
15   # '>' trigger 2 actions
16   verify_output "modify file" 
17   verify_output "modify file" 
18 
19   echo "last line" >> "$file"
20   echo "modify $file :" 
21   # '>>' triger only 1
22   verify_output "modify file"
23 
24   mv "$file" "$newfile"
25   echo "move $file :" 
26   verify_output "rename file"
27 
28   rm "$newfile"
29   echo "remove $file :" 
30   verify_output "remove file"
31 }

關於 windows 與 mac 的檔案變更通知的對比,參見本文後記。

目錄變更

目錄的場景比較多,主要是和檔案結合後衍生了許多混合場景,對於每個用例組,都需要單獨呼叫一下:

test_file_changes "test.txt"
test_dir_changes_1
test_dir_changes_2
test_dir_changes_3
test_case_insensitive

出於篇幅考慮,下面這裡只列出最基本的場景 test_dir_changes_1。

macOS

 1 test_dir_changes_1()
 2 {
 3   local dir="abc"
 4   local newdir="def"
 5   local file="a.txt"
 6   local newfile="b.txt"
 7 
 8   mkdir "$dir"
 9   echo "mkdir $dir :"
10   verify_output "add dir"
11 
12   touch "$dir/$file"
13   echo "touch $dir/$file :"
14   verify_output "add file" 
15 
16   echo "first line" > "$dir/$file"
17   echo "modify $dir/$file :" 
18   # '>' trigger 2 actions for newly created file
19   verify_output "add file" 
20   verify_output "modify file" 
21 
22   echo "last line" >> "$dir/$file"
23   echo "modify $dir/$file :" 
24   # '>>' triger 2 actions, too
25   verify_output "add file" 
26   verify_output "modify file"
27 
28   mv "$dir/$file" "$dir/$newfile"
29   echo "move $dir/$file to $dir/$newfile:" 
30   verify_output "remove file"
31   verify_output "add file"
32 
33   mv "$dir" "$newdir"
34   echo "move $dir to $newdir :"
35   verify_output "remove dir"
36   verify_output "add dir"
37 
38   rm "$newdir/$newfile"
39   echo "remove $newdir/$newfile :"
40   verify_output "remove file"
41 
42   rmdir "$newdir"
43   echo "remove $newdir :"
44   verify_output "remove dir"
45 }

基本場景和檔案差不多。

Windows

 1 test_dir_changes_1()
 2 {
 3   local dir="abc"
 4   local newdir="def"
 5   local file="a.txt"
 6   local newfile="b.txt"
 7 
 8   mkdir "$dir"
 9   echo "mkdir $dir :"
10   verify_output "add dir"
11 
12   touch "$dir/$file"
13   echo "touch $dir/$file :"
14   # touch trigger 3 actions
15   verify_output "add file" 
16   verify_output "modify file"
17   verify_output "modify dir"
18 
19   echo "first line" > "$dir/$file"
20   echo "modify $dir/$file :" 
21   # '>' trigger 2 actions
22   verify_output "modify file" 
23   verify_output "modify file" 
24 
25   echo "last line" >> "$dir/$file"
26   echo "modify $dir/$file :" 
27   # '>>' triger only 1
28   verify_output "modify file"
29 
30   mv "$dir/$file" "$dir/$newfile"
31   echo "move $dir/$file :" 
32   verify_output "rename file"
33   verify_output "modify dir"
34 
35   mv "$dir" "$newdir"
36   echo "move $dir :"
37   verify_output "rename dir"
38 
39   rm "$newdir/$newfile"
40   echo "remove $newdir/$newfile :"
41   verify_output "remove file"
42   verify_output "modify dir"
43 
44   rmdir "$newdir"
45   echo "remove $newdir :"
46   verify_output "remove dir"
47 }

目錄測試會在內部建立數量不等的子目錄和檔案,由外面傳入的話一是比較麻煩、二是限制內部邏輯不夠靈活,因此都在內部指定好了,呼叫時不需要額外引數。

彙總

所有用例跑完後需要向用戶展示通過與不通過用例的個數,這個很好實現,每個用例都會用到 verify_output,就把它整合在這裡吧:

 1 pass_cnt=0
 2 fail_cnt=0
 3 
 4 # $1: expect
 5 verify_output()
 6 {
 7     local expect="$1"
 8     local line=""
 9     read -r line <&6 # -r: stop transform '\'
10     local str=${line/"$expect"//}
11     if [ "$str" == "$line" ]; then 
12         # no change, character string not find.
13         echo "[FAIL] $line" 
14         fail_cnt=$(($fail_cnt+1))
15         return 1
16     else
17         echo "[PASS] $line" 
18         pass_cnt=$(($pass_cnt+1))
19         return 0
20     fi
21 }

新增了 line 14 和 18,兩個全域性變數用來記錄總的成功與失敗用例數量,最後將它們打印出來:

echo ""
echo "test done, $pass_cnt PASS, $fail_cnt FAIL"
echo "press any key to exit..."

羅列了這麼多指令碼,下面看一下完整的輸出效果。

macOS

$ sh rdc_main.sh
rm: *.fifo: No such file or directory
init fifo
init dir: tmp
start work process
start pumping
[SKIP] dir: /Users/yunhai01/test/rdc-test/tmp
[SKIP] create worker thread 0x16b3c3000
[READY] start monitoring...
touch test.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/test.txt
modify test.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/test.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/test.txt
modify test.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/test.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/test.txt
move test.txt new.test.txt :
find added flag with renamed path, ignore..
find modify flag with removed path, ignoring
[PASS] remove file: /Users/yunhai01/test/rdc-test/tmp/test.txt
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/new.test.txt
remove test.txt :
find removed flag with renamed path, ignore..
[PASS] remove file: /Users/yunhai01/test/rdc-test/tmp/new.test.txt
mkdir abc :
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/abc
touch abc/a.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/abc/a.txt
modify abc/a.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/abc/a.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/abc/a.txt
modify abc/a.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/abc/a.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/abc/a.txt
move abc/a.txt to abc/b.txt:
find added flag with renamed path, ignore..
find modify flag with removed path, ignoring
[PASS] remove file: /Users/yunhai01/test/rdc-test/tmp/abc/a.txt
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/abc/b.txt
move abc to def :
find added flag with renamed path, ignore..
[PASS] remove dir: /Users/yunhai01/test/rdc-test/tmp/abc
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/def
remove def/b.txt :
[PASS] remove file: /Users/yunhai01/test/rdc-test/tmp/def/b.txt
remove def :
find removed flag with renamed path, ignore..
[PASS] remove dir: /Users/yunhai01/test/rdc-test/tmp/def
mkdir a :
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/a
mkdir a/aa :
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/a/aa
touch a/a1.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/a/a1.txt
modify a/a1.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/a/a1.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/a/a1.txt
create & modify a/aa/a2.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/a/aa/a2.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/a/aa/a2.txt
mkdir b :
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/b
mkdir b/bb :
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/b/bb
touch b/b1.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/b/b1.txt
cp a/a1.txt b/b1.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/b/b1.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/b/b1.txt
cp a/aa/a2.txt b/bb/b2.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/b/bb/b2.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/b/bb/b2.txt
modify b/b1.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/b/b1.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/b/b1.txt
modify b/bb/b2.txt :
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/b/bb/b2.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/b/bb/b2.txt
move b/b1.txt to a/aa/a2.txt :
find added flag with renamed path, ignore..
find modify flag with removed path, ignoring
find added flag with renamed path, ignore..
[PASS] remove file: /Users/yunhai01/test/rdc-test/tmp/b/b1.txt
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/a/aa/a2.txt
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/a/aa/a2.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/a/aa/a2.txt
move a/a1.txt to b/bb :
find added flag with renamed path, ignore..
find modify flag with removed path, ignoring
[PASS] remove file: /Users/yunhai01/test/rdc-test/tmp/a/a1.txt
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/b/bb/a1.txt
remove b/bb/a1.txt :
find removed flag with renamed path, ignore..
[PASS] remove file: /Users/yunhai01/test/rdc-test/tmp/b/bb/a1.txt
remove b/bb/b2.txt :
find add & remove flag appears together, ignoring add
find modify flag with removed path, ignoring
[PASS] remove file: /Users/yunhai01/test/rdc-test/tmp/b/bb/b2.txt
remove a/aa/a2.txt :
find removed flag with renamed path, ignore..
[PASS] remove file: /Users/yunhai01/test/rdc-test/tmp/a/aa/a2.txt
move a/aa to b/bb
find added flag with renamed path, ignore..
[PASS] remove dir: /Users/yunhai01/test/rdc-test/tmp/a/aa
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/b/bb/aa
move b to a
find added flag with renamed path, ignore..
[PASS] remove dir: /Users/yunhai01/test/rdc-test/tmp/b
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/a/b
remove a/b/bb/aa :
[PASS] remove dir: /Users/yunhai01/test/rdc-test/tmp/a/b/bb/aa
remove a/b/bb :
[PASS] remove dir: /Users/yunhai01/test/rdc-test/tmp/a/b/bb
remove a/b :
find removed flag with renamed path, ignore..
[PASS] remove dir: /Users/yunhai01/test/rdc-test/tmp/a/b
remove a :
find add & remove flag appears together, ignoring add
[PASS] remove dir: /Users/yunhai01/test/rdc-test/tmp/a
prepare dir tree...
mv /tmp/a a :
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/a
move a to b :
[PASS] remove dir: /Users/yunhai01/test/rdc-test/tmp/a
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/b
move b to /tmp/a :
[PASS] remove dir: /Users/yunhai01/test/rdc-test/tmp/b
copy /tmp/a :
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/a
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/a/a1.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/a/a1.txt
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/a/a2.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/a/a2.txt
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/a/aa
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/a/aa/aa2.txt
[PASS] modify file: /Users/yunhai01/test/rdc-test/tmp/a/aa/aa2.txt
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/a/aa/aa1.txt
[FAIL] add file: /Users/yunhai01/test/rdc-test/tmp/a/aa/aa1.txt
remove a :
[FAIL] modify file: /Users/yunhai01/test/rdc-test/tmp/a/aa/aa1.txt
find add & remove flag appears together, ignoring add
find modify flag with removed path, ignoring
find add & remove flag appears together, ignoring add
find modify flag with removed path, ignoring
find add & remove flag appears together, ignoring add
find modify flag with removed path, ignoring
find add & remove flag appears together, ignoring add
find modify flag with removed path, ignoring
find add & remove flag appears together, ignoring add
find add & remove flag appears together, ignoring add
[PASS] remove file: /Users/yunhai01/test/rdc-test/tmp/a/a1.txt
[PASS] remove file: /Users/yunhai01/test/rdc-test/tmp/a/a2.txt
[PASS] remove file: /Users/yunhai01/test/rdc-test/tmp/a/aa/aa2.txt
[FAIL] remove file: /Users/yunhai01/test/rdc-test/tmp/a/aa/aa1.txt
[PASS] remove dir: /Users/yunhai01/test/rdc-test/tmp/a/aa
touch aBc :
[FAIL] remove dir: /Users/yunhai01/test/rdc-test/tmp/a
move aBc to AbC :
find added flag with renamed path, ignore..
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/aBc
[PASS] add file: /Users/yunhai01/test/rdc-test/tmp/AbC
remove AbC :
find removed flag with renamed path, ignore..
[PASS] remove file: /Users/yunhai01/test/rdc-test/tmp/AbC
mkdir def :
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/def
move def to DEF :
find added flag with renamed path, ignore..
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/def
[PASS] add dir: /Users/yunhai01/test/rdc-test/tmp/DEF
rmdir DEF :
find removed flag with renamed path, ignore..
[PASS] remove dir: /Users/yunhai01/test/rdc-test/tmp/DEF

test done, 79 PASS, 4 FAIL
press any key to exit...

wait worker process...
finished, close fifo
cleanup
all done

其中有些事件的先後順序有隨機性,不可避免的會有一些失敗,只要確認這些點沒有異常就可以了。

Windows

$ sh rdc_main.sh
rm: cannot remove '*.fifo': No such file or directory
init fifo
init dir: tmp
start work process
start pumping
[SKIP] dir: D:/test/rdc-test/tmp
[READY] start monitoring...
touch test.txt :
[PASS] add file: D:/test/rdc-test/tmp\test.txt
[PASS] modify file: D:/test/rdc-test/tmp\test.txt
modify test.txt :
[PASS] modify file: D:/test/rdc-test/tmp\test.txt
[PASS] modify file: D:/test/rdc-test/tmp\test.txt
modify test.txt :
[PASS] modify file: D:/test/rdc-test/tmp\test.txt
move test.txt :
[PASS] rename file from D:/test/rdc-test/tmp\test.txt to D:/test/rdc-test/tmp\new.test.txt
remove test.txt :
[PASS] remove file: D:/test/rdc-test/tmp\new.test.txt
mkdir abc :
[PASS] add dir: D:/test/rdc-test/tmp\abc
touch abc/a.txt :
[PASS] add file: D:/test/rdc-test/tmp\abc\a.txt
[PASS] modify dir: D:/test/rdc-test/tmp\abc
[PASS] modify file: D:/test/rdc-test/tmp\abc\a.txt
modify abc/a.txt :
[PASS] modify file: D:/test/rdc-test/tmp\abc\a.txt
[PASS] modify file: D:/test/rdc-test/tmp\abc\a.txt
modify abc/a.txt :
[PASS] modify file: D:/test/rdc-test/tmp\abc\a.txt
move abc/a.txt :
[PASS] rename file from D:/test/rdc-test/tmp\abc\a.txt to D:/test/rdc-test/tmp\abc\b.txt
[PASS] modify dir: D:/test/rdc-test/tmp\abc
move abc :
[PASS] rename dir from D:/test/rdc-test/tmp\abc to D:/test/rdc-test/tmp\def
remove def/b.txt :
[PASS] remove file: D:/test/rdc-test/tmp\def\b.txt
[PASS] modify dir: D:/test/rdc-test/tmp\def
remove def :
[FAIL] remove file: D:/test/rdc-test/tmp\def
mkdir a :
[PASS] add dir: D:/test/rdc-test/tmp\a
mkdir a/aa :
[PASS] add dir: D:/test/rdc-test/tmp\a\aa
[PASS] modify dir: D:/test/rdc-test/tmp\a
touch a/a1.txt :
[PASS] add file: D:/test/rdc-test/tmp\a\a1.txt
[PASS] modify dir: D:/test/rdc-test/tmp\a
[PASS] modify file: D:/test/rdc-test/tmp\a\a1.txt
modify a/a1.txt :
[PASS] modify file: D:/test/rdc-test/tmp\a\a1.txt
create & modify a/aa/a2.txt :
[PASS] add file: D:/test/rdc-test/tmp\a\aa\a2.txt
[PASS] modify dir: D:/test/rdc-test/tmp\a\aa
[PASS] modify file: D:/test/rdc-test/tmp\a\aa\a2.txt
[PASS] modify file: D:/test/rdc-test/tmp\a\aa\a2.txt
mkdir b :
[PASS] add dir: D:/test/rdc-test/tmp\b
mkdir b/bb :
[PASS] add dir: D:/test/rdc-test/tmp\b\bb
[PASS] modify dir: D:/test/rdc-test/tmp\b
touch b/b1.txt :
[PASS] add file: D:/test/rdc-test/tmp\b\b1.txt
[PASS] modify dir: D:/test/rdc-test/tmp\b
[PASS] modify file: D:/test/rdc-test/tmp\b\b1.txt
cp a/a1.txt b/b1.txt :
[PASS] modify file: D:/test/rdc-test/tmp\b\b1.txt
[PASS] modify file: D:/test/rdc-test/tmp\b\b1.txt
cp a/aa/a2.txt b/bb/b2.txt :
[PASS] add file: D:/test/rdc-test/tmp\b\bb\b2.txt
[PASS] modify dir: D:/test/rdc-test/tmp\b\bb
[PASS] modify file: D:/test/rdc-test/tmp\b\bb\b2.txt
modify b/b1.txt :
[PASS] modify file: D:/test/rdc-test/tmp\b\b1.txt
modify b/bb/b2.txt :
[PASS] modify file: D:/test/rdc-test/tmp\b\bb\b2.txt
move b/b1.txt a/aa/a2.txt :
[PASS] remove file: D:/test/rdc-test/tmp\a\aa\a2.txt
[PASS] remove file: D:/test/rdc-test/tmp\b\b1.txt
[PASS] add file: D:/test/rdc-test/tmp\a\aa\a2.txt
[PASS] modify dir: D:/test/rdc-test/tmp\a\aa
[PASS] modify dir: D:/test/rdc-test/tmp\b
move a/a1.txt b/bb :
[PASS] remove file: D:/test/rdc-test/tmp\a\a1.txt
[PASS] add file: D:/test/rdc-test/tmp\b\bb\a1.txt
[PASS] modify dir: D:/test/rdc-test/tmp\b\bb
[PASS] modify dir: D:/test/rdc-test/tmp\a
remove b/bb/a1.txt :
[PASS] remove file: D:/test/rdc-test/tmp\b\bb\a1.txt
[PASS] modify dir: D:/test/rdc-test/tmp\b\bb
remove b/bb/b2.txt :
[PASS] remove file: D:/test/rdc-test/tmp\b\bb\b2.txt
[PASS] modify dir: D:/test/rdc-test/tmp\b\bb
remove a/aa/a2.txt :
[PASS] remove file: D:/test/rdc-test/tmp\a\aa\a2.txt
[PASS] modify dir: D:/test/rdc-test/tmp\a\aa
move a/aa b/bb
[FAIL] remove file: D:/test/rdc-test/tmp\a\aa
[PASS] add dir: D:/test/rdc-test/tmp\b\bb\aa
[PASS] modify dir: D:/test/rdc-test/tmp\b\bb
[PASS] modify dir: D:/test/rdc-test/tmp\a
move b a
[FAIL] remove file: D:/test/rdc-test/tmp\b
[PASS] add dir: D:/test/rdc-test/tmp\a\b
[PASS] modify dir: D:/test/rdc-test/tmp\a
remove a/b/bb/aa :
[FAIL] remove file: D:/test/rdc-test/tmp\a\b\bb\aa
[PASS] modify dir: D:/test/rdc-test/tmp\a\b\bb
remove a/b/bb :
[FAIL] remove file: D:/test/rdc-test/tmp\a\b\bb
[PASS] modify dir: D:/test/rdc-test/tmp\a\b
remove a/b :
[FAIL] remove file: D:/test/rdc-test/tmp\a\b
[PASS] modify dir: D:/test/rdc-test/tmp\a
remove a :
[FAIL] remove file: D:/test/rdc-test/tmp\a
prepare dir tree...
mv ../a a :
[PASS] add dir: D:/test/rdc-test/tmp\a
move a to b :
[PASS] rename dir from D:/test/rdc-test/tmp\a to D:/test/rdc-test/tmp\b
move b to ../a :
[FAIL] remove file: D:/test/rdc-test/tmp\b
copy ../a a:
[PASS] add dir: D:/test/rdc-test/tmp\a
[PASS] add file: D:/test/rdc-test/tmp\a\a1.txt
[PASS] modify dir: D:/test/rdc-test/tmp\a
[PASS] modify file: D:/test/rdc-test/tmp\a\a1.txt
[PASS] add file: D:/test/rdc-test/tmp\a\a2.txt
[PASS] modify dir: D:/test/rdc-test/tmp\a
[PASS] modify file: D:/test/rdc-test/tmp\a\a2.txt
[PASS] add dir: D:/test/rdc-test/tmp\a\aa
[FAIL] add file: D:/test/rdc-test/tmp\a\aa\aa1.txt
[FAIL] modify dir: D:/test/rdc-test/tmp\a\aa
[FAIL] modify file: D:/test/rdc-test/tmp\a\aa\aa1.txt
[FAIL] add file: D:/test/rdc-test/tmp\a\aa\aa2.txt
[FAIL] modify dir: D:/test/rdc-test/tmp\a\aa
[FAIL] modify file: D:/test/rdc-test/tmp\a\aa\aa2.txt
[FAIL] modify dir: D:/test/rdc-test/tmp\a
remove a :
[PASS] remove file: D:/test/rdc-test/tmp\a\a1.txt
[PASS] modify dir: D:/test/rdc-test/tmp\a
[PASS] remove file: D:/test/rdc-test/tmp\a\a2.txt
[PASS] modify dir: D:/test/rdc-test/tmp\a
[PASS] remove file: D:/test/rdc-test/tmp\a\aa\aa1.txt
[FAIL] modify file: D:/test/rdc-test/tmp\a\aa
[FAIL] remove file: D:/test/rdc-test/tmp\a\aa\aa2.txt
[FAIL] modify file: D:/test/rdc-test/tmp\a\aa
[FAIL] remove file: D:/test/rdc-test/tmp\a\aa
[FAIL] modify file: D:/test/rdc-test/tmp\a
[FAIL] remove file: D:/test/rdc-test/tmp\a
touch aBc :
[PASS] add file: D:/test/rdc-test/tmp\aBc
move aBc to AbC :
[PASS] modify file: D:/test/rdc-test/tmp\aBc
[PASS] remove file: D:/test/rdc-test/tmp\aBc
[PASS] rename file from D:/test/rdc-test/tmp\aBc to D:/test/rdc-test/tmp\AbC
remove AbC :
[PASS] remove file: D:/test/rdc-test/tmp\AbC
mkdir def :
[PASS] add dir: D:/test/rdc-test/tmp\def
move def to DEF :
[PASS] remove dir: D:/test/rdc-test/tmp\def
[PASS] rename dir from D:/test/rdc-test/tmp\def to D:/test/rdc-test/tmp\DEF
rmdir DEF :
[FAIL] remove file: D:/test/rdc-test/tmp\DEF

test done, 89 PASS, 22 FAIL
press any key to exit...

wait worker process...
finished, close fifo
cleanup
all done

windows 上也存在同樣的問題,而且由於之前程式碼的問題,會將某種目錄型別的通知弄錯為檔案,所以導致這裡錯誤有點多。一般遇到這種情況,沒有什麼好辦法,多跑幾次應該能獲得好看一點的資料。另外從總數上看 windows 為 111 個檢查,mac 上為 83 個,這多出來的 28 個應該是檔案變更時直接目錄的 modify dir 通知,在 mac 上是沒有的。關於更多的 windows 與 mac 檔案變更通知的差異,參見本文後記。

錯誤處理

這個指令碼對輸出的要求比較高,如果不能嚴格實現一個動作對應 N 個輸出檢查,那麼可能就會發生阻塞 (輸出條數少於檢查條數) 和混亂 (輸出條數多於檢查條數),特別是 mac 平臺,有時會將多條通知合併成一條送達,儘管引擎會做一些過濾工作,但難免有漏網之魚。這些事對於程式而言不是什麼嚴重問題,大不了多做一次無用功,但對自動化指令碼可就麻煩了,輕則對不齊,重則卡死,對不齊時會導致後面一系列 case 失敗,卡死的話雖然可以通過 Ctrl + C 退出,但是每次要手動清理臨時檔案,非常麻煩。為了解決這個問題,這裡提供了一個指令碼用於通知 rdc_main.sh 正常退出:

 1 #! /bin/sh
 2 resp=""
 3 while true
 4 do
 5     cnt=$(find . -type p -name "*.out.fifo" | wc -l)
 6     if [ $cnt -eq 0 ]; then 
 7         echo "no pipe like *.out.fifo, exit"
 8         break
 9     fi
10 
11     for i in `ls *.out.fifo`
12     do
13         echo "send msg to $i"
14         echo "start failed" >> "$i"
15     done
16 
17   # send next msg when user press 'Enter'
18   read resp
19 done
20 
21 echo "all done"

簡單解釋一下:

  • line 5:獲取當前檔案所有 out-fifo 管道檔案;
  • line 11-15:對這些管道,傳送一條訊息,使卡在上面的 read 返回。訊息內容可以保證對應的 rdc_main 不會 pass 相應的用例;
  • line 18: 等待使用者輸入任意字元,迴圈上述過程,當 rdc_main 退出後,會清理相應的測試目錄和 fifo 管道,此時 find 找不到 fifo 結尾的檔案,就會自動退出這個指令碼;
  • 也可以注掉 line 18,這樣就可以實現自動退出的效果,不論有多少未執行的 test case 都可以跳過。

在 main 指令碼所在的目錄輸入下面的命令即可:

1 ./rdc_quit.sh

早期寫用例的時候,會經常遇到這種場景,後來磨合好了就用的少了。

後記

本文說明了一種在特定場景下使用 shell 指令碼做自動化測試的方法,並不適用於通用化的場景,對於後者還是要求助於各種測試工具和框架。另外這裡待測試的目標是一個獨立執行的引擎 demo,而不是程式語言中的方法或類,所以歸屬於自動化測試而非單元測試,這裡使用單元測試的話也是可行的,但那樣就需要編譯用例程式碼了,使用上不如這樣來的方便一些。

在探索的過程中踩到了 msys2 前後臺程序的坑,以及 console 重定向行緩衝問題,在上面浪費了不少時間,真正寫用例反而快一些。

編寫測試用例的過程中,又發現了 windows 與 mac 在檔案變更通知方面的差異,主要表現為:

  • windows 檔案變更時會有直屬目錄的變更通知,mac 沒有;
  • windows 有檔案重新命名的通知,mac 也有,但不健全,最後出於穩定性考慮,全部替換為 add+remove;
  • windows 對於移入移出的目錄,只通知到目錄本身,mac 會通知目錄下的每個檔案;
  • ……

關於更多的對比,敬請期待後面寫一篇單獨的文章說明。

下載

完整的自動化測試指令碼可點選下面的連結下載:

https://files-cdn.cnblogs.com/files/goodcitizen/rdc-test.tar.gz

其中不含 demo 程式,主要是出於以下考慮:

  • windows demo 依賴專案的一些基礎設施,釋出不便且有版權問題;
  • mac demo 為 M1 晶片編譯的 arm 版本,intel 晶片跑不了;

而自己動手做一個 demo 並不是什麼難事,特別是有現成的開源庫可以參考:

https://github.com/emcrisostomo/fswatch

當時 mac 端的引擎實現就是參考了這個。這個專案由 libfswatch 庫和 fswatch 命令組成,後者編譯完就是一個活脫脫的 demo,大家可以試一下。不過看了它在 windows 上的實現,居然直接用 ReadDirChanges 而沒用 iocp 分發事件,只能說開源的東西也就那樣吧,和工業級的要求還是有差距的,可以拿來參考參考,直接做專案還是差了一截。

參考

[1]. C/C++ 的全緩衝、行緩衝和無緩衝

[2]. 後臺程序讀/寫控制檯觸發SIGTTIN/SIGTTOU訊號量

[3]. 有名管道(FIFO)通訊機制完全攻略

[4]. buffering in standard streams

[5]. SHELL中的使用fifo管道實現多程序執行效率

[6]. 淺析Windows命名管道Named Pipe

[7]. setbuf與setvbuf函式

[8]. Shell read命令:讀取從鍵盤輸入的資料

[9]. 如何從Bash指令碼中檢測作業系統?

[10]. https://github.com/emcrisostomo/fswatch