前言
專案中有一個功能,需要監控本地檔案系統的變更,例如檔案的增、刪、改名、檔案資料變動等等,之前只在 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
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/388940.html
標籤:其他
