読者です 読者をやめる 読者になる 読者になる

JavaのライブラリのテストをJRuby+minitestで書く

お前は何と戦っているんだ感が半端ないですが、タイトルの通りJavaで書いたライブラリのテストをJRuby+minitestで書いてみました。

テストを書く対象のライブラリは、よくある(?)行列演算を行うやつです。 例としてMyMatrixLib.jar (net.rkmathi.lib.MyMatrixLib)という名前にしてみます。

JRuby, minitestのインストール

rbenvを使っているので、それ経由で入れました。

$ rbenv install -l | grep jruby
...
jruby-1.7.16
jruby-1.7.16.1
jruby-9.0.0.0-dev
jruby-9000-dev
...

jruby-1.7.16.1にしました。

$ rbenv install jruby-1.7.16.1

テスト書くディレクトリに移動して、rbenv localjrubyを使うようにします。

$ cd /path/to/test_directory
$ rbenv local jruby-1.7.16.1
$ rbenv rehash
$ jruby --version
jruby 1.7.16.1 (1.9.3p392) 2014-10-28 4e93f31 on Java HotSpot(TM) 64-Bit Server VM 1.8.0_25-b17 +jit [darwin-x86_64]

minitestのインストールをします。

$ jruby -S gem install minitest

ライブラリのJARを持ってくる

$ cd /path/to/test_directory
$ cp /path/to/MyMatrixLib.jar .

JRuby+minitestでテストを書く

とりあえず、get()とplus()とminus()のテストを書いてみるとこうなります。

require 'minitest'
require 'minitest/autorun'

require 'java'
require_relative './MyMatrixLib.jar'

import 'net.rkmathi.lib.MyMatrixLib'

class TestMyMatrixLib < Minitest::Test
  def setup
    @d1 = rand(1000.0)
    @d2 = rand(1000.0)
    @m1 = MyMatrixLib.new(3, 2, @d1)  # 3x2ですべての要素が@d1の行列を生成
    @m2 = MyMatrixLib.new(3, 2, @d2)  # 3x2ですべての要素が@d2の行列を生成
  end

  # get(r, c) => double: 行列のr行c列目の要素を取得
  def test_get
    each_assert_get @m1, @d1
    each_assert_get @m2, @d2
  end

  # plus(matrix) => MyMatrix: 行列のそれぞれの要素を足した行列を生成
  def test_plus
    d_res = @d1 + @d2
    m_res = @m1.plus(@m2)  # @m1と@m2のそれぞれの要素を足した結果の行列
    each_assert_get m_res, d_res
  end

  # minus(matrix) => MyMatrix: 行列のそれぞれの要素を引いた行列を生成
  def test_minus
    d_res = @d1 - @d2
    m_res = @m1.minus(@m2)  # @m1と@m2のそれぞれの要素を引いた結果の行列
    each_assert_get m_res, d_res
  end

  private

  # 行列の各要素をそれぞれgetするのが面倒なのでメソッド化
  def each_assert_get(m, d)
    for r in 0...m.get_row_dimension do
      for c in 0...m.get_column_dimension do
        assert_equal m.get(r, c), d
      end
    end
  end

  ...

end

と、こんな感じで書くことができます。

ちなみにJUnitだと...

public class MyMatrixLibTest {
    @Before
    public void setUp() throws Exception {
        MyMatrixLib d1 = Math.random() * 1000.0;
        MyMatrixLib d2 = Math.random() * 1000.0;
        MyMatrixLib m1 = new MyMatrixLib(3, 2, d1);
        MyMatrixLib m2 = new MyMatrixLib(3, 2, d2);
    }

    @Test
    public void testGet() {
        eachAssertGet(m1, d1);
        eachAssertGet(m2, d2);
    }

    @Test
    public void testPlus() {
        double d_res = d1 + d2;
        MyMatrixLib m_res = m1.plus(m2);
        eachAssertGet(m_res, d_res);
    }

    @Test
    public void testMinus() {
        double d_res = d1 - d2;
        MyMatrixLib m_res = m1.minus(m2);
        eachAssertGet(m_res, d_res);
    }

    private void eachAssertGet(MyLibMatrix m, double d) {
        for (int row = 0; row < m.getRowDimension(); row++) {
            for (int column = 0; column < m.getRowDimension(); column ++) {
                assertEquals(m.get(row, column), d);
            }
        }
    }
}

簡単な演算のテスト部分しか比較しませんでしたが、書いてて楽には思えました。

ですが、JRubyを書いていると文法とか変数の型をミスった時に吐かれる例外がよくわからないことになることもあるので、わざわざJRubyでテスト書くのは微妙かも...

VPS上にSoftEther VPN ServerをセットアップしてOS Xからつなぐ

HDD容量を少なくすればめちゃ安いVPS( http://www.idcf.jp/cloud/ )を教えてもらったので、ここにSoftEtherVPN Serverをセットアップしてみました。

ちなみに、仮想マシンを作成するときに「テンプレート」からだとHDD容量が最低15GBなので540円/月ですが、「ISO」から作るようにすると最低1GBから選べるようになります。例えば5GBにすれば324円/月で済みます。

ポートの設定

VPSのコントロールパネル側でファイアウォールの設定があるので、

のポートを開けておきます。

SoftEther VPNのダウンロード

SoftEther ダウンロードセンターから、SoftEther VPN (Freeware)SoftEther VPN ServerLinuxIntel x64 / AMD64 (64bit)を選んで、最新ビルドのリンクのアドレスをメモっておきます。

あとで、OSをインストールしたらこのリンクから引っ張ってきます。

OSのインストールとファイルの配置

適当に、Ubuntu Server 14.04 amd64をインストールしておきました。

### 必要なパッケージのインストール
~$ sudo aptitude update
~$ sudo aptitude upgrade
~$ sudo aptitude install build-essential sysv-rc-conf

### さっきのリンクからダウンロード
~$ wget http://jp.softether-download.com/files/XXX/softether-vpnserver-XXX-linux-x64-64bit.tar.gz
~$ tar xvf softether-vpnserver-XXX-linux-x64-64bit.tar.gz
~$ cd vpnserver

### 確認したら、1でAcceptしてmake
~/vpnserver$ make
1
1
1

### vpnserverのパーミッションを変更
~/vpnserver$ chmod 600 *
~/vpnserver$ chmod 700 vpncmd vpnserver
~/vpnserver$ cd ../
~$ sudo mv vpnserver /usr/local/
~$ sudo chown -R root:root /usr/local/vpnserver
~$ cd /usr/local/vpnserver

### 動作確認
/usr/local/vpnserver$ sudo ./vpncmd
3
check
exit

/usr/local/vpnserver$ cd

### initスクリプトを書いて、パーミッションを755にしておく
~$ sudo chmod 755 /etc/init.d/vpnserver
~$ sudo cat /etc/init.d/vpnserver
#!/bin/sh
# description: SoftEther VPN Server
DAEMON=/usr/local/vpnserver/vpnserver
LOCK=/var/lock/vpnserver
test -x $DAEMON || exit 0
case "$1" in
start)
$DAEMON start
touch $LOCK
;;
stop)
$DAEMON stop
rm $LOCK
;;
restart)
$DAEMON stop
sleep 3
$DAEMON start
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
esac
exit 0

### sysv-rc-confでvpnserverの2,3,4,5にチェック
~$ sudo sysv-rc-conf
...

### 再起動
~$ sudo reboot

SoftEther VPN Serverのセットアップ

~$ cd /usr/local/vpnserver

/usr/local/vpnserver$ sudo ./vpncmd
1
localhost
(Enter)

### VPN Serverのパスワードを設定
VPN Server> ServerPasswordSet
(パスワードを設定)

### 仮想ハブを作成 ここではVPNHUBという名前にしてある
VPN Server> HubCreate
Name of Virtual Hub to be created: VPNHUB

### VPNHUBに切り替え
VPN Server> Hub VPNHUB

### SecureNatを有効に
VPN Server/VPNHUB> SecureNatEnable

### ユーザを作成 ここではvpnという名前にしてある
VPN Server/VPNHUB> UserCreate
vpn
(Enter)
(Enter)
(Enter)

### 上で作ったvpnユーザのパスワードを設定
VPN Server/VPNHUB> UserPasswordSet
vpn
(VPN User Password)
(VPN User Password)

### IPSecを有効に
VPN Server/VPNHUB> IPSecEnable
Enable L2TP over IPsec Server Function (yes / no): yes
Enable Raw L2TP Server Function (yes / no): no
Enable EtherIP / L2TPv3 over IPsec Server Function (yes / no): no
Pre Shared Key for IPsec (Recommended: 9 letters at maximum): (IPsec Secret Key)
Default Virtual HUB in a case of omitting the HUB on the Username: VPNHUB

VPN Server/VPNHUB> exit

$ sudo service vpnserver restart

OS X(クライアント)側の設定

システム環境設定ネットワーク→左下の+マークで追加

f:id:rkmathii:20141128000937p:plain

↑画像のサーバアドレスアカウント名を埋めたら、下にある認証設定…をクリック

f:id:rkmathii:20141128001148p:plain

ユーザ認証 パスワードに(VPN User Password)を、コンピュータ認証 共有シークレットに(IPsec Key)を入力

これで接続ができるように(┐「ε:)

GitLabでMaven Repositoryを作ってGradleから使う

研究室でJavaのプログラムを書いているのですが、ライブラリを研究室内で共有するときにMaven Repositoryを立てたらいいのではないかと思ったので、GitLab上で使うようにしました。

想定するプログラム

  • ライブラリ(MyLib)
  • MyLibを使用するプログラム(MyProgram)

MyLibのbuild.gradle

apply plugin: 'idea'
apply plugin: 'java'
apply plugin: 'maven'

def defaultEncoding = 'UTF-8'
[compileJava, compileTestJava]*.options*.encoding = defaultEncoding
sourceCompatibility = 1.7
targetCompatibility = 1.7

group = 'net.rkmathi'
archivesBaseName = 'mylib'
version = '1.0.0'

repositories {
    maven { url System.getenv('HOME') + '/.m2/repository' }
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit:4.11'
}

uploadArchives.repositories.mavenDeployer {
    repository(url: "file:${projectDir}/maven")
}

これでgradle uploadArchivesを実行すると、mavenディレクトリ以下に出力されます。

  • uploadArchivesで出力したら、gitリポジトリにそのディレクトリも追加して、GitLabへプッシュします。

MyProgramのbuild.gradle

apply plugin: 'idea'
apply plugin: 'java'
apply plugin: 'maven'

def defaultEncoding = 'UTF-8'
[compileJava, compileTestJava]*.options*.encoding = defaultEncoding
sourceCompatibility = 1.7
targetCompatibility = 1.7

group = 'net.rkmathi'
archivesBaseName = 'myprogram'
version = '1.0.0'

repositories {
    maven { url System.getenv('HOME') + '/.m2/repository' }
    maven { url 'http://<<GitLab Address>>/<<User Name>>/mylib/raw/master/maven' }
    mavenCentral()
}

dependencies {
    compile 'net.rkmathi:mylib:1.0.0'
    testCompile 'junit:junit:4.11'
}

uploadArchives.repositories.mavenDeployer {
    repository(url: "file:${projectDir}/maven")
}

これで、MyProgramからMyLibを使うことができました。

  • http://<<GitLabのアドレス>>/<<ユーザ名>>/<<プロジェクト名>>/raw/mastermaven repositoriesの場所に指定することによって、GitLab上にあるライブラリを取得できるようになります。

ワーイ

ISUCON4本戦に参加したけどダメでした

ISUCON4の本戦に@cnosukeさんと@k0kubunさんの3人で、「railsへの執着はもはや煩悩」というチームで出てきました。

cnosukeさんのブログ( http://cnosuke.hatenablog.com/entry/2014/11/11/141321 )やk0kubunさんのブログ( http://k0kubun.hatenablog.com/entry/2014/11/09/003455 )に状況とか詳しく書いてあるのであんまり書くことないですが、とりあえず最終結果は6,000点も行かずにダメダメな感じでした(ヽ´ω`)

戦略

f:id:rkmathii:20141109151912p:plain

動画広告の配信だし、多分ベンチマークから攻撃のようなアクセスをされるだろうと思い、3台与えられたサーバ全部に対してアクセスさせるようにしました。

ただし、各サーバともメモリが1GBしか積んでいなかったためスワップしまくりだし、OOM Killerにunicornだのvarnishだのを殺されて辛い感じでした。

反省とか

ISUCONの敵であるbenchmarkerの挙動をもっとちゃんと把握しておくべきでした。

benchmarkerからのアクセスログを覗いていたら、"Galaxy Nexus"とかいろんな端末のUAがあって「なんかbenchmarkerめちゃ凝ってるぽいなー」とは思ったのですが、まさかブラウザの挙動を模してキャッシュ機能とかまで備えているとは考えられませんでした。

何か問題があってそれに対して高速化を図るのに、その問題をきちんと把握せずに取り組むのは今更ですが良くなかったです(ヽ´ω`)

(でも競技時間が8時間しかないので焦ってしまう…)

おわり

今年で学生は終わりなので最後の学生枠だったのですが、本戦まで行けて(一応)最終結果をFailせずに終われたのはよかったです。

来年も出場したいので、社会人枠で戦えるようにちゃんと勉強していきます。

ISUCONは競技中はずっと緊張感があり、とても楽しかったです。

運営をしてくださった皆様に感謝しております。本当にありがとうございました。

今年もISUCON4予選に参加した→予選通過できた!

去年のISUCON3にも参加してダメだったんですが、今年はISUCON夏期講習に参加してから、ISUCON4の予選に参加しました。

学生枠「railsへの執着はもはや煩悩の域であり、開発者一同は瞑想したほうがいいと思います。」というチーム名で、id:cnosukeさんとid:k0kubunさんの3人で出場しました。

k0kubunさんの記事→http://k0kubun.hatenablog.com/entry/2014/09/28/202828

学生枠は上位5チームが本戦に出場できるのですが、予選1日目の時点で4位だったので本戦に行けるかはびみょいです(;´Д`)


ちなみに、使った言語はRubyでした。

開発環境まわりでやったこと

各自で自分専用のインスタンスを立ち上げる

m3.xlargeインスタンスが$0.4/1hくらいだったので、各自でインスタンス立ち上げてそこで開発/ベンチマークが終わったら、GitHubにプルリ投げて本番用インスタンスベンチマーク走らせるってフローでやってました。

GitHub

去年、開発中にベンチとったらいつの間にかFailしまくってしまい、どこで間違えたのかわからなくなってしまったので、今年は GitHubのプライベートリポジトリを立ててそこでプルリ投げ合いながら開発することにしました。

事前に3人で集まって練習した時は1つのインスタンスを3人で触っていたので、どこを変更したのかよくわからなくなってました。 ですが、この方法で割と安全に開発が進められたと思いました。

実装まわりでやったこと

erbをslimに置き換えた

事前にISUCON2の問題を練習していたときに、erbよりもslimのほうが速かったのでそうしました。

'/'はnginxで静的HTMLを返すようにした

トップページもslimで描画でしていたのですが、出力HTMLは変わってなかったのでnginxで生HTMLを返すようにしました。

これと、他の2人がやってくれたvanishやMySQLのインデックスなどでだいたいスコアが8000点前後でした。


反省点

プルリ体制にしてたのに自分がミスった

プルリを投げるときは手元で動かしたベンチマークの結果も一緒に貼って、ちゃんとベンチマークが落ちないことを確かめていたのですが、自分が/reportをいじった時のプルリでベンチマークが落ちていたのにメッセージに気づくことができなかったです。 そのため、後半で「実はベンチマークとおってない」ことに気づいてから、ロールバックするのに時間がかかってしまいました(;´Д`)

ルール(というかツール)をちゃんと把握しておくべきだった

ベンチを走らせるときに、編集したinit.shが何故か適用されていなくて困惑してたら、土壇場になって--helpつけたらちゃんとオプションがあることに気づきました。 また、その時にworkloadについても気づけました(;´Д`) モッタイナカッタ…

m(_ _)m


本戦に行けるかどうかはまだ分かってないですが、今年のISUCON予選もとても楽しかったです!!!

10/06追記: 学生枠4位通過で、本戦に参加できることになりました!!!やった!!!

運営をしてくださった方々にはとてもお世話になりました。

ありがとうございました!!!