2 failure test_gdbm.rb on Ruby-1.8.7_p160

test_dbm.rbのテストはALL GREENになったが、今度は同gdbm.dllを使用するtest_gdbm.rbのテストが通らないのが気になる所だ。

E:\ruby-1.8.7-p160\test\gdbm>ruby test_gdbm.rb
Loaded suite test_gdbm
Started
.....................F...F.................
Finished in 0.546875 seconds.

  1) Failure:
test_reorganize(TestGDBM) [test_gdbm.rb:594]:
 expected but was
.

  2) Failure:
test_s_open_create_new(TestGDBM) [test_gdbm.rb:87]:
<420> expected but was
<438>.

43 tests, 945 assertions, 2 failures, 0 errors

通らなかったテストは2つ。

  • test_reorganize
   def test_reorganize
      size1 = File.size(@path)
      i = "1"
      1000.times {i = i.next; @gdbm[i] = i}
      @gdbm.clear
      @gdbm.sync

      size2 = File.size(@path)
      @gdbm.reorganize
      size3 = File.size(@path)

      # p [size1, size2, size3]
      assert_equal(true, size1 < size2)
      # this test is failed on Cygwin98. `GDBM version 1.8.0, as of May 19, 1999'
●    assert_equal(true, size3 < size2)
      assert_equal(size1, size3)
    end

Cygwin98ではテストが通らなかったと注釈が付いている。CygwinではなくWindowsも通らないのだが、後述する。

  • test_s_open_create_new
    def test_s_open_create_new
      return if /^CYGWIN_9/ =~ SYSTEM

      save_mask = File.umask(0)
      begin
        assert_instance_of(GDBM, gdbm = GDBM.open("tmptest_gdbm"))
        gdbm.close
●      assert_equal(File.stat("tmptest_gdbm").mode & 0777, 0666)
        assert_instance_of(GDBM, gdbm = GDBM.open("tmptest_gdbm2", 0644))
        gdbm.close
        assert_equal(File.stat("tmptest_gdbm2").mode & 0777, 0644)
      ensure
        File.umask save_mask
      end
    end

Windowsの場合対応していないモードビットがあるため、こちらのテストは通らなくても仕方がないようだ。
Win32ネイティブ版Rubyの互換性問題

という訳で調査はtest_reorganizeだけとする。

GDBM#reorganizeだが、ソースを読む限りこれはデータベースファイルの再編成を行う処理を実行すると考えて良いだろう。GDBMの普通の実装においてはデータを削除してもファイルのサイズはそれに合わせて小さくはならない。(削除されたデータのスペースは再利用のために予約される)
reorganize(以降、再編成と呼ぶ)は新たにファイルを作りデータを格納し直すことで結果としてファイルスペースを小さくする。ここでのテストは再編成前と後のデータベースファイルのサイズを比べて、再編成後の方が小さくなっていることを証明するものだ。

size2 = File.size(@path)
@gdbm.reorganize
size3 = File.size(@path)
:
assert_equal(true, size3 < size2)

テストが失敗するのは、reorganizeの前後でサイズが変わっていないからだ。
やはり、GDBMの実装に問題があると考えるべきだろう。

ソースを見る限り、reorganizeは、

1. 新たなデータベース(ファイル)の作成
2. 元のデータベース上のデータを新たなデータベースにコピー
3. 新たなデータベースのファイル名を、元のデータベースのファイル名にリネーム
4. 新しいデータベースの情報で元のデータベース情報を上書き

これにより結果として再編成を実施したことになるのだが、問題は3.のリネーム処理だ。

  • dbm.dll#gdbmreorg.c ※
int
gdbm_reorganize (dbf)
     gdbm_file_info *dbf;

{
  〜略
  if (rename (new_name, dbf->name) != 0)
    {
      gdbm_errno = GDBM_REORGANIZE_FAILED;
      gdbm_close (new_dbf);
      free (new_name);
      return -1;
    }
 〜

処理としては再編成後のDBを使うために、新DBのファイル名を元DBのファイル名にリネームしている。ここで期待される振る舞いは元のファイル名に変更することと、元のファイルを再編成したファイルで上書きしてしまうことだが、UNIX Cでは期待通りの動きをするものの、WindowsというO/Sでは既存のファイルを上書きするリネームはエラーとなってしまうので、このままでは上手くいかない(-1 returnする)。

さてどうしよう。

renameが使えないのであれば、同様の振る舞いを実現する代替えの関数に変えれば良い。stdioの標準関数renameはWin32ではMoveFileにマップされているがWin32には得意の"Ex"関数が用意されている。

MoveFileEx関数

関数の名前からしてファイルを移動する関数であることが分かるが、移動前の名前と移動後の名前をパラメタとして渡すため、実際にはrenameと同じことを実現できる。さらに、MoveFileExの"Ex"たる所以は、追加のオプションパラメタに以下のオプションを持つことだ。

MOVEFILE_REPLACE_EXISTING

このオプションを指定することで移動後のファイル名を持つファイルが既に存在していた場合、移動前のファイルで内容を置き換えることができる。これならばUNIXのrenameと同じ振る舞いに出来るはずだ。

ということで修正して試してみたい所だが、このrenameを以下のようにそのままMoveFileExに変えても良い所なのだが、

if ( MoveFileEx(old_name, new_name, MOVEFILE_REPLACE_EXISTING) != 0 )
  { 〜

自前修正といっても、元々gdbmreorgのソースコードはWindows用のヘッダも書きたくないので、前回修正したwin32.cに取り敢えず書くことにした。

  • win32.c
#include 
#include 
#include 

int	
frename (const char* old_name, const char* new_name)
  {
    return MoveFileEx(old_name, new_name, MOVEFILE_REPLACE_EXISTING);
  }

関数名をfrenameにしているのは標準関数と名前が重複してしまうため。ヘッダなどで再定義する方法もあるが、影響範囲が分からないのでまずはアドホックに対応することにした。
さて、ではこいつを呼ぶgdbm_reorganizeの修正部分だが、frenameと関数名を修正しているため現状はプリプロセッサを使わざるを得ない。

#ifdef	WIN32
  if (frename (new_name, dbf->name) != 0)
  {
#endif	
#ifndef	WIN32
  if (rename (new_name, dbf->name) != 0)
  {
#endif	
    gdbm_errno = GDBM_REORGANIZE_FAILED;
    gdbm_close (new_dbf);
    return -1;
  }

これで完璧と思いきや、ビルドしてgdbm.dllを作り直して実際に動かしてみると上手くいかない。GetLastErrorで調べてみると32が戻っている。

プロセスはファイルにアクセスできません。別のプロセスが使用中です。

(Code:00000032)

どうやらMoveFileExという関数は移動先のファイルに上書きできるとあるものの、そのファイルが開かれたままであれば当然失敗するということらしい。ならば、対象のファイルを前もってクローズしておくしかない。ただしこのクローズ動作はWindows以外のプラットホームでは不要な操作なので、ここでもまたプリプロセッサに頼ることになる。

最終的にgdbmreorg.cは以下のようになった。

  • gdbmreorg.c.diff
@@ -161,16 +133,6 @@
   gdbm_sync (new_dbf);
 
 
-  /* Move the new file to old name. */
-
-  if (rename (new_name, dbf->name) != 0)
-    {
-      gdbm_errno = GDBM_REORGANIZE_FAILED;
-      gdbm_close (new_dbf);
-      free (new_name);
-      return -1;
-    }
-
   /* Fix up DBF to have the correct information for the new file. */
   if (dbf->file_locking)
     {
@@ -180,6 +142,33 @@
   free (dbf->header);
   free (dbf->dir);
 
+    /* Move the new file to old name. */
+#ifdef	WIN32
+  gdbm_close (new_dbf);	  
+  if (frename (new_name, dbf->name) != 0)
+  {
+    gdbm_errno = GDBM_REORGANIZE_FAILED;
+#endif	
+#ifndef	WIN32
+  if (rename (new_name, dbf->name) != 0)
+  {
+    gdbm_errno = GDBM_REORGANIZE_FAILED;
+    gdbm_close (new_dbf);
+#endif	
+    return -1;
+  }
+#ifdef	WIN32
+  new_dbf = gdbm_open (dbf->name, dbf->header->block_size, 
+      GDBM_WRCREAT, fileinfo.st_mode, dbf->fatal_err);
+
+  if (new_dbf == NULL)
+  {
+    gdbm_errno = GDBM_REORGANIZE_FAILED;
+    return -1;
+  }
+#endif	
+
+
   if (dbf->bucket_cache != NULL) {
     for (index = 0; index < dbf->cache_size; index++) {
       if (dbf->bucket_cache[index].ca_bucket != NULL)

うー、やっぱり#ifdef使いまくりだとソースが読みづらい。何かもっと良い方法無いかなと思うのだが、、、

さて、Ruby側に戻りテストを実施してみよう。

E:\ruby-1.8.7-p160\test\gdbm>ruby test_gdbm.rb
Loaded suite test_gdbm
Started
.........................F.................
Finished in 7.583332 seconds.

  1) Failure:
test_s_open_create_new(TestGDBM) [test_gdbm.rb:87]:
<420> expected but was
<438>.

43 tests, 946 assertions, 1 failures, 0 errors

OKだ。
File.modeに関してはどうしようも無いので、放置するか内部でターゲットO/Sによって無視するようにテストを書き換える位だろうか。


結局、前回同様に原因はUNIXとWindowsのCにおける標準関数の振る舞いの違いだった。
素朴な疑問だが、CygWinとかMinGwとかはrenameの実装はどうしているんだろう。


GNUスタイルのインデントは未だに馴染まないな。