メモリ上でI/OテストしてSSDに優しくなる

# SSDに優しくなりたい

行単位でstage/unstageできるgit GUIクライアントを作っています。
[週刊 git GUIクライアントを作る [1] stage/unstage基礎知識編](https://qiita.com/gitcommitamend/items/f19d05b8db5390b9b647)
[週刊 git GUIクライアントを作る [2] stage/unstage不完全攻略編](https://qiita.com/gitcommitamend/items/3463d8f569c390be6801)

gitを使うアプリケーションなので、そのテストのためにファイルの読み書きが発生します。
**コードに対する不安**があり、そして**テストコードを書くのが初めてで楽しくて調子に乗ってる**ため、任意の状況での行の選択パターンを網羅したテストというのを行っていて、現時点では70秒ぐらいかかります。

70秒もずっとファイル読み書きするテストをコードの変更のたびにやっていると「SSDに優しくないな」という気持ちになりました。ビルドもテストも使い捨てるものなので、メモリ上でやれたら嬉しいですね。ちょっと調べてみると、以下の方法で良さそうなので試してみました。

# 方法

1. RAM Diskを作る
2. ビルドの出力先をRAM Diskに変更する

## 対象環境

+ Windows
+ Gradleプロジェクト

今回使うのはImDiskとgradleだけなので、これ以外に制限は無いはずです。
他の環境でも同様のことは可能だろうと思います。

## IOを伴うテストについて

ほかにいい方法があるのかもしれませんが、
現在はgradleプロジェクトでJunit5を使って次のようにテストしています。

+ src\test\resources\以下に、操作対象とするファイルを用意する
+ それがコピーされたbuild\resources\test以下で、ファイルとgitの操作を行う
+ テストリソースとしての.gitフォルダを持ちたくないので、毎回git initから行う

このため、ビルドの出力先をメモリ上に持っていくだけで、目的が達成されます。
なお、実用性は求めていませんが、70秒のテストが60秒ぐらいになりました。



# RAM Diskを作る

[ImDisk Virtual Disk Driver](http://www.ltr-data.se/opencode.html/#ImDisk)というのを使いました。
GUI版の[ImDisk Toolkit](https://sourceforge.net/projects/imdisk-toolkit/)もいい感じでした。
    
参考記事
http://siwon-g.hateblo.jp/entry/20151122/1448195973
http://jinblog.at.webry.info/201402/article_2.html


# ビルドの出力先を変更する

+ build.graldeで`project.buildDir`変数を変更します。
+ ソースと異なるドライブでも、万事うまくいく雰囲気です。

# build.graldeにまとめる

+ 指定のドライブがマウントされているかどうか確認する

 - JavaのFile.listRoots()でドライブの一覧を取得できます

+ マウントされていなければRAM Diskを作ってマウントする

 - 例えば`'imdisk -a -s 2g -t vm -o rem -p "/fs:ntfs /q /y" -m ' + G:`
 - `-s 2g`オプションでサイズ指定します。`-s -2g`のように負の値を指定して、2gを残してそれ以外をめいっぱい使う、ということもできるようです。。
 - `-o rem`オプションでリムーバブルドライブにしています。そうでない場合、管理者権限が必要とされたり、強制アンマウントが必要になったりします。
 - `-p "/fs:ntfs /q /y"`オプションでファイルをフォーマットできます。なお、GUIクライアントのImDisk ToolkitでRAMディスクを作ってマウントすると、Windowsがフォーマットするかどうか尋ねてきてくれます。
 - `-m G:`オプションでマウント先を指定します。試してませんが、`-m #:`と書くと、空いてるドライブ文字を選択してくれます。

```build.gradle
def TARGET_DRIVE = 'G:'
def dir = TARGET_DRIVE + '\\project_name\\build'
if (hasDrive(TARGET_DRIVE)) {
    buildDir = dir 
} else {
    def COMMAND = 'imdisk -a -s 2g -t vm -o rem -p "/fs:ntfs /q /y" -m ' + TARGET_DRIVE
    println "Mounting RAM Disk to $TARGET_DRIVE is ..."
    def ret = COMMAND.execute().waitFor()
    if (ret == 0 && hasDrive(TARGET_DRIVE)) {
        println "done by command: "
        buildDir = dir 
    } else {
        println 'failed by command: '
    }
    println ' ' + COMMAND
}
println 'buildDir: ' + buildDir

// 指定のドライブがマウントされているか
static boolean hasDrive(String letter) {
    def drive = File.listRoots().find {
        return it.absolutePath.startsWith(letter)
    }
    return drive != null && drive.exists()
}

```

ロジックはリモートにpushしてもいいかもしれませんが、ドライブ文字の選択は個人でも環境によって異なるので、そのままだと微妙かもしれません。少しだけ考えてみると、次のような案が思いつきました。

+ `.gitignore`したローカルファイルにドライブ文字を書けるようにする
+ `#:`を使って、ビルドのたびに作成・削除する

# （追記）設定ファイルを利用する

>`.gitignore`したローカルファイルにドライブ文字を書けるようにする

上述の案を実際に書いてみました。
config.groovyに設定を書きます。

+ `RAM.drive`に書かれたドライブ文字を利用してRAM diskを作成
+ `RAM.drive`が無ければ何もせず、通常のbuildDirを利用
+ `RAM.size`が設定されていばそのサイズを指定、無ければデフォルトのサイズ1GBでRAM diskを作成
+ `RAM.size`が設定されていばその形式でフォーマット、無ければデフォルトのntfsでフォーマット

```config.groovy
RAM {
    drive = 'G:'
    size = '2g'
}
```
```/.gitignore
config.groovy
```
```build.gradle
def config = new ConfigSlurper().parse(file('config.groovy').text)
def TARGET_DRIVE = config.RAM.drive ?: ''
if (TARGET_DRIVE) {
    def dir = TARGET_DRIVE + '\\project_name\\build'
    if (hasDrive(TARGET_DRIVE)) {
        buildDir = dir
    } else {
        def size = config.RAM.size ?: "1g"
        def format = config.RAM.format ?: "ntfs"
        def COMMAND = "imdisk -a -s $size -t vm -o rem -p \"/fs:$format /q /y\" -m " + TARGET_DRIVE
        println "Mounting RAM Disk to $TARGET_DRIVE is ..."
        def ret = COMMAND.execute().waitFor()
        if (ret == 0 && hasDrive(TARGET_DRIVE)) {
            println "done by command: "
            buildDir = dir
        } else {
            println 'failed by command: '
        }
        println ' ' + COMMAND
    }
}
println 'buildDir: ' + buildDir
```

groovyはエルビス演算子`?:`が使えて気持ちいいですね。