Mac OS X El Capitanにrlwrapをソースからインストールする

つまづいたところのメモ。

rlwrapをbrewからインストールしようとしたら、パッケージのダウンロードで失敗していた。

% brew install rlwrap                                                                          [master]
==> Downloading http://utopia.knoware.nl/~hlub/rlwrap/rlwrap-0.42.tar.gz

curl: (7) Failed to connect to utopia.knoware.nl port 80: Operation timed out
Error: Failed to download resource "rlwrap"
Download failed: http://utopia.knoware.nl/~hlub/rlwrap/rlwrap-0.42.tar.gz

utopia.knoware.nlの名前解決もできない残念な状況。

仕方ないのでソースから入れることにする。

git clone https://github.com/hanslub42/rlwrap
cd rlwrap
autoreconf --install
./configure

しかしmakeに失敗する。

% make                                                                                        [994/1376]
/Applications/Xcode.app/Contents/Developer/usr/bin/make  all-recursive
Making all in doc
sed -e 's#@DATADIR@#/usr/local/share#'  rlwrap.man > rlwrap.1
Making all in src
gcc -DHAVE_CONFIG_H -I. -I..    -DDATADIR=\"/usr/local/share\"  -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
main.c:813:15: error: use of undeclared identifier 'rl_basic_quote_characters'
    case 'q': rl_basic_quote_characters = mysavestring(optarg); break;
              ^
1 error generated.
make[2]: *** [main.o] Error 1
make[1]: *** [all-recursive] Error 1
make: *** [all] Error 2

readlineライブラリにあるはずの関数がないと言っている。調べてみると、Mac OS Xに入っているreadlineライブラリはeditlineというライブラリへのリンクになっているらしい。一方で、rlwrapはGNU readline(が定義している関数)を必要としている。

そういうわけで、GNU readlineをインストールして、そのパスをconfigure時に教えてあげる必要がある。

brew install readline
./configure LDFLAGS="-L/usr/local/opt/readline/lib" CPPFLAGS="-I/usr/local/opt/readline/include"
make
make install

通った。

参考にしたページ

luciでAppEngine/Goの異常系テストを手軽に書く

こんばんは。アドベントカレンダー Google Cloud Platform(2) 24日目担当の @ks888 です。

昨日の記事に続いて、AppEngine/Goのテストに便利なluci/gaeというライブラリを紹介していきます。昨日は簡単な紹介として、AppEngine/Goのテスト実行時間を短縮する例を出しました。今日は、特に異常系のテストを書くのに役立つ、filter機能とmockのリプレース機能を紹介したいと思います。

filter機能

filterはdatastoreやtaskqueue等のサービスへの操作をプロキシできる機能で、サービスへのアクセス前後で何か処理を行いたいときに役立ちます。luci/gaeには、memcacheをdatastoreの手前に挟むfilterや、サービスへの操作回数をカウントするfilter等が付属しています。もちろん、自分でfilterを作ることもできます。

今回はテストに役立つfilterとして、featureBreakerというfilterを使ってみます。featureBreakerを使うと、サービスの操作に対して強制的に任意のエラーを返させることができます。

前回と同じですが、以下のようなコードを考えます。/userにPOSTされたデータをDatastoreに書き込んでいます。

package main

import (
    "encoding/json"
    "net/http"

    "github.com/luci/gae/impl/prod"
    "github.com/luci/gae/service/datastore"
    "golang.org/x/net/context"
    "google.golang.org/appengine"
)

const userKind = "User"

// User ...
type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

// UserEntity ...
type UserEntity struct {
    User
    Key *datastore.Key `gae:"$key"`
}

func init() {
    http.HandleFunc("/user", userHandler)
}

func userHandler(w http.ResponseWriter, r *http.Request) {
    ctx := prod.Use(appengine.NewContext(r), r)
    switch r.Method {
    case "POST":
        handlePostUser(ctx, w, r)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func handlePostUser(ctx context.Context, w http.ResponseWriter, r *http.Request) {
    var user User
    err := json.NewDecoder(r.Body).Decode(&user)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer r.Body.Close()

    key := datastore.NewKey(ctx, userKind, user.Email, 0, nil)
    err = datastore.Put(ctx, &UserEntity{user, key})
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusOK)
}

これに対して、datastoreへのPutがエラーを返すケースのテストが以下のように書けます。

func Test_HandlePostUser_DatastoreError(t *testing.T) {
    ctx := memory.Use(context.Background())
    ctx, fb := featureBreaker.FilterRDS(ctx, nil)
    fb.BreakFeatures(nil, "PutMulti")

    response := httptest.NewRecorder()
    json := `{"name": "mahiru", "email": "inami@example.com"}`
    request, _ := http.NewRequest("POST", "/user", strings.NewReader(json))

    handlePostUser(ctx, response, request)

    if response.Code != http.StatusInternalServerError {
        t.Fatalf("unexpected code: %d", response.Code)
    }
}

3, 4行目でfeatureBreakerを使っています。PutMultiというメソッドが呼ばれたら、エラーを返すように設定しました。今回はデフォルトのエラーを返す設定にしましたが、任意のエラーを返すこともできます。これだけでエラーケースのテストが書けるのは楽ですね。

ここでちょっとややこしいのが、テストされる側のコードではdatastoreのPut関数を呼んでいるのに、featureBreakerではPutMultiメソッドに対してエラーを返す設定をしています。

これはluci/gaeの設計上致し方ないかなと思っています。というのも、luci/gaeはユーザー向けのインターフェース内部用のインターフェースをもっており、ユーザー向けのインターフェースの実装中で内部用のインターフェースを呼ぶ、という実装になっています。そして、filterは内部用のインターフェースに対するプロキシとして動作します。

今回のケースでも、Put関数はユーザー向けのインターフェースで、内部ではPutMultiメソッドを呼んでいます。そのため、Put関数にエラーを返させたいときは、PutMultiメソッドに対してfeatureBreakerを設定してあげる必要があります。

mockのリプレース機能

luci/gaeでは、luciが提供するdatastoreやTaskQueueのモックの代わりに、自分で定義したモックを使うこともできます。datastore等に思い通りの挙動をさせることができるので、通常の利用では起きにくい状況、あるいは起きないはずの状況を想定したテストを作成するのに役立ちます。

まずはモックを作ってみます。datastoreのモックとして認識してもらうためは、こちらのインターフェースを実装した構造体が必要です。一つ一つ実装してもいいですが、luci/gaeのdummyパッケージを使うと、独自実装したいメソッドだけ実装すればいいので楽です。

type datastoreMock struct {
    datastore.RawInterface
    putMultiCallCount int
}

func newDatastoreMock() datastore.RawInterface {
    return &datastoreMock{dummy.Datastore(), 0}
}

func (m *datastoreMock) PutMulti(keys []*datastore.Key, vals []datastore.PropertyMap, cb datastore.NewKeyCB) error {
    m.putMultiCallCount++

    if m.putMultiCallCount <= 1 {
        return errors.New("failed to put entities")
    }
    return nil
}

PutMultiメソッドだけ、自分で実装しています。ここでは、1回目の呼び出しは必ずエラーを返すようにしてみました。リトライのテストとかで使えるかなーと思います。

このモックは、以下のように使います。

func Test_HandlePostUser_Retry(t *testing.T) {
    ctx := memory.Use(context.Background())
    ctx = datastore.SetRaw(ctx, newDatastoreMock())

    response := httptest.NewRecorder()
    json := `{"name": "mahiru", "email": "inami@example.com"}`
    request, _ := http.NewRequest("POST", "/user", strings.NewReader(json))

    handlePostUser(ctx, response, request)

    if response.Code != http.StatusOK {
        t.Fatalf("unexpected code: %d", response.Code)
    }
}

3行目のSetRaw()でモックを独自のものに入れ替えています。

かなり雑ですが、テストされる側のコードもPut操作をリトライするように書き換えます。

func handlePostUser(ctx context.Context, w http.ResponseWriter, r *http.Request) {
    var user User
    err := json.NewDecoder(r.Body).Decode(&user)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer r.Body.Close()

    key := datastore.NewKey(ctx, userKind, user.Email, 0, nil)
    err = datastore.Put(ctx, &UserEntity{user, key})
    if err != nil {
        err = datastore.Put(ctx, &UserEntity{user, key})
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    }

    w.WriteHeader(http.StatusOK)
}

テストを実行してみます。

% goapp test ./...
ok      gaetest/luci    0.038s

ちゃんと動いているようです。独自のモックを作成することで、リトライが発生するケースのテストも簡単に作成できました。

まとめ

luci/gaeのfilter機能とmockのリプレース機能を使ったテストの例を紹介しました。luci/gaeはとにかくドキュメントが足りてない印象があるので、この記事が参考になれば幸いです。

AppEngine/Goのテストを100倍高速化するライブラリluciの紹介

こんばんは。アドベントカレンダー Google Cloud Platform(2) 23日目担当の @ks888 です。

突然ですが、luciというライブラリをご存知でしょうか。AppEngine/Goのテストに効果を発揮する素晴らしいライブラリなのですが、あまり使っているという話を聞きません。今回はそんな不遇?のライブラリluciを紹介してみたいと思います。

luciとは

luciのGitHubページには次のように書かれています。

LUCI stands for Layered "Universal" Continuous Integration.
It's a fleet of complementary, but independent, scalable services
made to facilitate small-to-huge-scale CI needs.

うーんCIに役立つサービス群とありますが、ピンと来ません。具体例として、luci/gaeというluciのサブプロジェクトの説明をみてみましょう。luci/gaeのgodocには、以下のように書いてあります。

Package gae provides a fakable wrapped interface for the appengine SDK's APIs.
This means that it's possible to mock all of the supported appengine APIs
for testing (or potentially implement a different backend for them).

Features

gae currently provides interfaces for:

- Datastore
- Memcache
- TaskQueue
- Info (e.g. Namespace, AppID, etc.)

luci/gaeは、AppEngineのAPIをラップしたインターフェースを提供していて、そのインターフェースの実装をモックに置き換えることで、テストがしやすくなるようです。実はluci/gaeはDatastoreやTaskQueue等のインメモリ実装を提供しており、これをモックとして利用することで、高速なテストの実行が可能になります。また、モックは自分で好きな実装に置き換えられるので、色々なテストケースに対応することができます。

luciのGitHubページに書かれていた「CIに役立つサービス群」とは、luci/gaeにおいてはAppEngine APIのラッパーインターフェース+そのインメモリ実装のことなのかと思います。

では、luci/gaeを使うとどのようにテストが書けるのか、以下で試してみます。なお、luciにはluci/gae以外にもサブプロジェクトがありますが、今回はluci/gaeにフォーカスします。

luci/gaeを使わずにテストする

まずは、luci/gaeを使わない場合のテストコードを確認してみます。

以下のようなAppEngine/Goのコードを考えます。/userにPOSTされたデータをDatastoreに書き込んでいるだけです。DatastoreのKeyとEmailがかぶっていますが、今回は簡単のためにこのままにします。

package main

import (
    "encoding/json"
    "net/http"

    "golang.org/x/net/context"

    "google.golang.org/appengine"
    "google.golang.org/appengine/datastore"
)

const userKind = "User"

// User ...
type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func init() {
    http.HandleFunc("/user", userHandler)
}

func userHandler(w http.ResponseWriter, r *http.Request) {
    ctx := appengine.NewContext(r)
    switch r.Method {
    case "POST":
        handlePostUser(ctx, w, r)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func handlePostUser(ctx context.Context, w http.ResponseWriter, r *http.Request) {
    var user User
    err := json.NewDecoder(r.Body).Decode(&user)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer r.Body.Close()

    key := datastore.NewKey(ctx, userKind, user.Email, 0, nil)
    _, err = datastore.Put(ctx, key, &user)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusOK)
}

対して、以下のようなテストコードを考えます。

package main

import (
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"

    "google.golang.org/appengine/aetest"
    "google.golang.org/appengine/datastore"
)

func Test_HandlePostUser(t *testing.T) {
    ctx, done, err := aetest.NewContext()
    if err != nil {
        t.Fatal(err)
    }
    defer done()

    response := httptest.NewRecorder()
    json := `{"name": "mahiru", "email": "inami@example.com"}`
    request, _ := http.NewRequest("POST", "/user", strings.NewReader(json))

    handlePostUser(ctx, response, request)

    if response.Code != http.StatusOK {
        t.Fatalf("unexpected code: %d", response.Code)
    }

    user := User{}
    datastore.Get(ctx, datastore.NewKey(ctx, userKind, "inami@example.com", 0, nil), &user)
    if user.Email != "inami@example.com" {
        t.Fatalf("invalid email: %s", user.Email)
    }
    if user.Name != "mahiru" {
        t.Fatalf("invalid name: %s", user.Name)
    }
}

テストを実行してみます。

% goapp test ./...
ok      gaetest 4.623s

テストは通りました。しかしこれだけのテストなのに、実行時間が4.623sになっています。なぜでしょうか?

答えは、テストコードの冒頭で呼び出しているaetest.NewContext()にあります。以下はaetestパッケージのドキュメントからの抜粋です。

func NewContext() (context.Context, func(), error)

NewContext starts an instance of the development API server, and
returns a context that will route all API calls to that server,
as well as a closure that must be called when the Context is no
longer required.

NewContext()を呼び出す度に開発用サーバが起動していて、そのためにテストの実行時間が長くなっているようです。

どうすればいいでしょうか?ここからのアプローチは色々あると思うのですが、その一つにluci/gaeがあります。

luci/gaeを使えば、開発用サーバの代わりにDatastoreやTaskQueueのモックを使ってテストすることができます。開発用サーバを立ち上げずに済むので、テストの実行時間が無駄に長くなりません。また、DatastoreやTaskQueueのモックは各テストで独立しているので、テスト間でDatastore操作等が競合しないように気を使う必要もありません。

luci/gaeを使ってテストする

では先程のコードをluci/gaeを使ってテストしてみます。luci/gaeはAppEngineのAPIをラップしたインターフェースを提供しているので、まずはテストされる側のコードをluci/gaeのインターフェースに沿って書き換えます。datastoreパッケージのgodocはこちらです。

以下のように書き換えました。

package main

import (
    "encoding/json"
    "net/http"

    "github.com/luci/gae/impl/prod"
    "github.com/luci/gae/service/datastore"
    "golang.org/x/net/context"
    "google.golang.org/appengine"
)

const userKind = "User"

// User ...
type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

// UserEntity ...
type UserEntity struct {
    User
    Key *datastore.Key `gae:"$key"`
}

func init() {
    http.HandleFunc("/user", userHandler)
}

func userHandler(w http.ResponseWriter, r *http.Request) {
    ctx := prod.Use(appengine.NewContext(r), r)
    switch r.Method {
    case "POST":
        handlePostUser(ctx, w, r)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func handlePostUser(ctx context.Context, w http.ResponseWriter, r *http.Request) {
    var user User
    err := json.NewDecoder(r.Body).Decode(&user)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer r.Body.Close()

    key := datastore.NewKey(ctx, userKind, user.Email, 0, nil)
    err = datastore.Put(ctx, &UserEntity{user, key})
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusOK)
}

主に変更したのは、context生成部分と、datastoreのKey指定部分です。

contextをprod.Use()という関数でラップすることで、luci/gaeに(モックではなく)実際のAppEngine環境を使うよう指定します。

また、datastoreのKeyは引数ではなく構造体のフィールドの一つとして指定します。上記コードのUserEntity構造体を定義している部分です。

次に、テストコードも書き換えます。

package main

import (
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"

    "github.com/luci/gae/impl/memory"
    "github.com/luci/gae/service/datastore"
    "golang.org/x/net/context"
)

func Test_HandlePostUser(t *testing.T) {
    ctx := memory.Use(context.Background())

    response := httptest.NewRecorder()
    json := `{"name": "mahiru", "email": "inami@example.com"}`
    request, _ := http.NewRequest("POST", "/user", strings.NewReader(json))

    handlePostUser(ctx, response, request)

    if response.Code != http.StatusOK {
        t.Fatalf("unexpected code: %d", response.Code)
    }

    user := UserEntity{Key: datastore.NewKey(ctx, userKind, "inami@example.com", 0, nil)}
    err := datastore.Get(ctx, &user)
    if err != nil {
        t.Fatal(err)
    }
    if user.Email != "inami@example.com" {
        t.Fatalf("invalid email: %s", user.Email)
    }
    if user.Name != "mahiru" {
        t.Fatalf("invalid name: %s", user.Name)
    }
}

こちらも、context生成部分と、datastoreのKey指定部分を変更しました。

contextをmemory.Use()という関数でラップすることで、luci/gaeにインメモリのAppEngine環境を使うよう指定します。

テストを実行してみます。

% goapp test ./...
ok      gaetest/luci    0.020s

実行時間が0.020sにまで短縮されました。開発用サーバの代わりにモックを使っているおかげで、余計なオーバーヘッドなくテストを実行できています。また、datastoreはcontextごとに独立しているので、テストの並列化もしやすいかと思います。

まとめ

簡単ですが、luci/gaeの使い方を紹介しました。AppEngine/Goのテストに役立ててもらえると幸いです。次回はテストを書くときに役立つluci/gaeの機能について紹介したいと思います。

更新:次回分書きました