POP3クライアントのテストを書いてみる

つい先程まで
嘘のような、本当の話を読んで、「いい話だなぁ」と感動し涙腺が緩みかけていたのですが、感動の余韻に浸る間もなく、引き続きOCamlでごにょごにょやってみたいと思います。

これまで、「OCamlでメールクライアントでも作ろうか」と気持ちを盛り上げて何とかやってきましたので、この薄い勢いにのって、POP3クライアントのモジュールでも作ってみようかと思います。

実は、OCamlにはocamlnetに含まれているpop(netpop.ml)というパッケージがあるのですが、 Unix.open_connectionで取得した入出力チャンネルを受け取るインターフェースで、SSLライブラリを滑り込ませられる余地がなさそうだったので、自作してみることにしました。要は当該パッケージをつかうとSSLの暗号化・復号化ができなさそう、という訳です。

早速、「POP3 RFC」で検索して、RFCの日本語訳に目を通してみました。すごく…コンパクトな内容です… RFCを読んでみたいという方には入門編として良いのではないかと思います。POP3サーバーは基本的にこのRFCの仕様に従いお返事してくるので、POP3クライアントではこの仕様を実装すればよいはず。

さて、POP3クライアントモジュールのテストを書いてみる前に、POP3クライアントモジュールの使い方(インターフェース)を決めちゃいます。

  1. オブジェクトを使う(new pop3_clientみたいな。理由⇒使ってみたいから)。
  2. オブジェクト生成時には、channelとかsocketとかfile_descrを直接渡さず、そういったものを隠蔽した入出力用の関数を渡す(Cだとfdを渡さず関数ポインタを渡す感じ)。
  3. RFCを読んじゃったので、一通りのコマンドを実装。
  4. とりあえず難しいことを考えず、コマンド毎にメソッドを用意。

2番の理由ですが、POP3サーバーとの入出力のためにchannelとかsocketとかfile_descrを直接渡しちゃうと、SSLの使用/未使用を切り替えられないので、こうしたほうがよいかなぁ、と。要は、このPOP3クライアントは通信周りについては何にも知らないよ〜、という関係にしたい。更に補足すると、Unix/Sslモジュールのread/writeメソッドにsocketとかfile_descrを部分適用した関数をイメージしています(上手くいくか分からないけど…)。

これを踏まえて、テストを書いてみます。四苦八苦しているところは例によって省略して、えいっ!

open OUnit
open Pop3

let overwrite dst src ofs =
  let size = String.length src in
  let rec loop dst src pos =
    if pos >= size then ()
    else begin
      dst.[pos + ofs] <- src.[pos];
      loop dst src (pos + 1)
    end in
  loop dst src 0;
  size

let dmy_infun l buf ofs bufsize =
  match !l with
  | [] -> 0
  | hd::tl -> (l := tl; overwrite buf hd ofs)

let dmy_outfun lst buf ofs bufsize =
  let tmp_buf = String.create 1024 in
  let wlen = overwrite tmp_buf buf ofs in
  let cmd = List.hd (Str.split (Str.regexp "\r\n") tmp_buf) in
  lst := !lst @ [cmd];
  wlen


let res_conn = ["Gpop ready for requests from xxx.xxx.xxx.xxx xxxxx.0"]
let res_user = ["send PASS"]
let res_pass = ["Welcome."]
let res_list = ["3 messages (687 bytes)"; "1 1831"; "2 2175"; "3 1829"]
let res_retr = ["message follows"; "Delivered-To: xxxx@gmail.com"; "Hello, world"]
let res_quit = ["dewey POP3 server signing off"]

let make_res_s l = List.hd l ^ "\r\n"
let make_res_m l = String.concat "\r\n" l ^ "\r\n.\r\n"

let res =
  ref ["+OK " ^ make_res_s res_conn; "+OK " ^ make_res_s res_user;
       "+OK " ^ make_res_s res_pass; "+OK " ^ make_res_m res_list;
       "+OK " ^ make_res_m res_retr; "+OK " ^ make_res_s res_quit]

let outlst = ref []
let clear_outlst () = outlst := []

let assert_lists la lb =
  List.iter2 (fun a b -> assert_equal a b) la lb


let obj = new pop3_client ~infun:(dmy_infun res) ~outfun:(dmy_outfun outlst)

let test_auth () =
  clear_outlst ();
  obj#auth ~user:"komamitsu" ~pass:"mypassword";
  assert_lists ["USER komamitsu"; "PASS mypassword"] !outlst

let test_list () =
  clear_outlst ();
  let res = obj#list () in
  assert_lists res_list res;
  assert_lists ["LIST"] !outlst

let test_retr () =
  clear_outlst ();
  let res = obj#retr 2 in
  assert_lists res_retr res;
  assert_lists ["RETR 2"] !outlst

let test_quit () =
  clear_outlst ();
  obj#quit ();
  assert_lists ["QUIT"] !outlst
;;

let suite = "Test Pop3" >:::
  ["test_auth" >:: test_auth; "test_list" >:: test_list;
   "test_retr" >:: test_retr; "test_quit" >:: test_quit]

let _ = run_test_tt_main suite

dmy_infun, dmy_outfunがテスト用のダミー入出力関数で、これをPOP3クライアント生成時に渡しておいて取り込んでもらい、各処理で読み出されたり書き込まれる内容をテストプログラム側で制御・確認しています。

ところで、今回ちょっと衝撃的だったことは「文字列はmutable(可変)」ということでした。 overwriteという関数でバッファの書き換えを行っているんですが、すぐ身近にあった型(文字列)がmutableだったのは意外でした。こういうのは実際にプログラミングしてみて改めて実感できますねぇ。

とりあえず、このテストにあうようにモジュールの枠だけを書いてコンパイルは通るようになったので、あとはモジュールの中身を書いていこうと思います。仕事のほうが忙しくなっちゃったので、なかなかOCamlをいじれていないですが、暇を作って進めてみたいと思いますです。