株式会社クリアコード > ククログ

ククログ


WerckerのDockerバックエンドを使ってCentOS向けCIをするには

クリアコードではHatoholというソフトウェアの開発に参加しています。Hatoholは複数の統合監視システムの情報を一括して表示することを可能とするソフトウェアです。現在対応している統合監視システムはZabbix、Nagios及びOpen StackのCeilometerです。他の監視システムに対応することも検討しています。

HatoholはServerとDjangoのWebApp、またユーザーのブラウザで動くJavaScript部分の3つで構成されています。

今回はHatohol serverのCIについてです。

HatoholプロジェクトではTravis CIを用いてCIをしています。Travis CIではUbuntu Serverが使われています。そのため、Ubuntuではビルドが通る事は確認できていました。しかし、CentOS 6系でもビルドが出来るかどうかのCIの仕組みがHatoholプロジェクトには入っていませんでした。

Werckerとは

WerckerとはTravis CIやCircle CIと並ぶCIサービスです。特に、workerが使うコンテナを自由にユーザーが用意する事ができ、自由度が高いのが特徴です。

今回Werckerが Dockerに対応したバックエンドを公開したので、HatoholのCentOS 6系向けCIがこのバックエンドで出来るかどうかを試してみました。

Werkcerの設定方法

WerckerはWerckerのアカウントを作成する方法と、GitHub認証によりWerckerと連携する方法があります。どちらの方法でも使い始める事が出来ます。

Werckerを使うにはリポジトリの直下にwercker.ymlを配置する必要があります。

また、WerckerでAppと呼ばれるものを作る必要があります。 このAppを起点にしてCIが始まります。*1

今回はWerckerのDockerバックエンドを試すので、wercker-labs/docker のbox *2 を使ったwercker.ymlを作成します。

box: wercker-labs/docker
build:
  steps:
    - script:
        name: docker version
        code: |
        docker pull centos:6.6

このようにしてやる事で、WerckerのDockerバックエンドを使ったboxがWerckerの中で動き出します。

Dockerfileを用いてDockerバックエンドworkerを走らせる

WerckerのDockerバックエンドでworkerを動かす際には、通常通りDockerfileを用いてdockerコンテナをビルドする事が可能です。

先ほどのwercker.ymlを次のように書き換えます。

box: wercker-labs/docker
build:
  steps:
    - script:
        name: docker version
        code: |
        docker build -t centos6-wercker .

また、リポジトリのwercker.ymlと同じ階層に Dockerfile を配置します。

from centos:6.6

maintainer clear-code

RUN rpm -ivh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm

ここまでで、次のようなリポジトリ構造になっています。

    $ tree
    .
    |-- Dockerfile
    `-- wercker.yml

ここまでの変更をpushしてみましょう。するとWerckerのworkerが動き出します。

HatoholプロジェクトでのWercker

HatoholプロジェクトではCentOS6.6のコンテナを用い、Hatoholのrpmビルドが通るかどうかのCIにWerckerを使い始めました。始めはビルドが通るかだけのCIだったのですが、ビルドの時間制限の25分を超えずにビルドが終了していたのでrpmのビルドも行うようにしました。 25分に制限されているというのは

Wercker - blog

.. you’re allowed to have any number of applications on wercker with a maximum of two concurrent builds, limited at 25 minutes per build each.

によります。アプリケーション数は無制限、アプリケーションのビルドは最大2並列まで、1ビルドにつき25分の制限、という制限が伺えます。

Hatoholプロジェクトでは次のような wercker.yml を作成し、

box: wercker-labs/docker
build:
  steps:
    - script:
        name: Build a CentOS 6.6 Docker Hatohol image box
        code: |
          docker -v
          docker build -t centos66/build-hatohol wercker/

wercker.yml とは同じ階層ではなくwerckerディレクトリの下にDockerfileを配置しています。

from centos:6.6

maintainer hatohol project

# install libraries for Hatohol
RUN yum install -y glib2-devel libsoup-devel sqlite-devel mysql-devel mysql-server \
  libuuid-devel qpid-cpp-client-devel MySQL-python httpd mod_wsgi python-argparse
# setup mysql
RUN echo "NETWORKING=yes" > /etc/sysconfig/network
RUN rpm -ivh --force http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
RUN yum install -y librabbitmq-devel wget
RUN wget -P /etc/yum.repos.d/ http://project-hatohol.github.io/repo/hatohol.repo
RUN yum install -y json-glib-devel qpid-cpp-server-devel
# setup qpid
RUN yum groupinstall -y 'Development tools'
RUN yum install -y Django
RUN rpm -Uvh http://sourceforge.net/projects/cutter/files/centos/cutter-release-1.1.0-0.noarch.rpm
RUN yum install -y cutter
# git clone
RUN git clone https://github.com/project-hatohol/hatohol.git ~/hatohol
# build
RUN cd ~/hatohol && libtoolize && autoreconf -i
RUN cd ~/hatohol && ./configure
RUN cd ~/hatohol && make -j `cat /proc/cpuinfo | grep processor | wc -l`
RUN cd ~/hatohol && make dist -j `cat /proc/cpuinfo | grep processor | wc -l`
# rpm build
RUN yum install -y rpm-build
RUN cd ~/hatohol && MAKEFLAGS="-j `cat /proc/cpuinfo | grep processor | wc -l`" rpmbuild -tb hatohol-*.tar.bz2

これにより、WerckerでCentOS 6.6コンテナが動き出します。

しばらく経つと、Dockerコンテナのビルドが完了します。

まとめ

WerckerのDockerバックエンドでCentOS 6系向けのCIの方法を解説しました。

Ubuntu Serverを用いたCIサービスは数多くありますが、CentOSの選択肢があるCIサービスはまだまだ少ないのが現状です。

Dockerコンテナ化されているLinuxディストリビューションであればDockerが動くCIサービスを用いて任意のディストリビューション向けのCIがDockerの上で出来るようになることが期待されます。

Ubuntu Server向けではなく、CentOS向けにもCIしたくなった時にWerckerのDockerバックエンドを用いてCentOSのDockerコンテナ上でCIを行う事を選択肢の一つとして検討してみるのはいかがでしょうか。

*1 何故Appと言われるかというとWerckerはCIだけではなくデプロイも含めたCIサービスだからです。

*2 werckerではworkerのジョブを走らせるコンテナをboxと呼びます。

2015-07-01

SEゼミ2015 - OSS Hack 4 Beginnersを開催 #sezemi

2015-06-27にOSS開発参加未経験学生向けOSS開発イベントであるOSS Hack 4 Beginnersを開催しました。このイベントについて、内容を作った立場からどうしてこのような内容にしたのかについて紹介します。また、今回の内容の課題と今後の解決案についてもまとめます。

目標

このイベントの目標は「OSS開発参加への不安を取りのぞくこと」にしました。参加者から不安が取りのぞかれていれば目標達成ということです。

この目標にした理由は、学生から「OSSの開発に参加したい気持ちはあるけど、漠然とした不安があって行動に移せていない」という声を聞いたからです。実際、イベントに参加した学生も同様の理由でOSSの開発の参加に至っていないということでした。

OSSの開発に参加することが当たり前の人視点では「とりあえず手を動かせばいいのに」と考えてしまいます。そのため、OSSの開発の参加に至っていない理由が前述の理由だということには思いもよりませんでした。勉強になりました。

OSSの開発に限らず、「漠然とした不安」で行動に移せていないケースは他にもあるのかもしれません。

内容の方針

「漠然とした不安」を取りのぞくため、次の方針の内容にしました。

  • 選択肢を減らす
  • 安全な環境を用意する
  • 実際に経験する

選択肢を減らす、特に、選択肢をオススメのもの1つにすることにより、「間違った選択肢を選んでしまう」という不安を排除し、正しい方向へ進んでいる感を与えます。選択肢がなくなった部分に関して自分で考えることが減ってしまうというデメリットがありますが、それは不安が解消し、余裕をもって行動できるようになってからでよいと割りきります。

なお、選択肢を減らすという方針はpixivこしばさんからのリーダブルコード勉強会へのフィードバックが元になっています。ありがとうございます。

安全な環境を用意するのは「うまくいかなくても大丈夫感」を与えて不安を排除するためです。選択肢を減らすのは「うまくいっている感」を与えて不安を排除するためでしたが、逆方向からも不安を排除するということです。

上述のように最初の1歩の敷居をさげ、実際に経験します。実際に経験することで、よくわからない「OSSの開発に参加」というものが具体的なものになります。具体的なものになれば、「漠然とした不安」は「心配することではなかった」と「具体的な困っていること」になります。「具体的な困っていること」は個別に具体的な対策を立てて解消できます。

内容

この方針を元に次の内容にしました。

  1. オススメの手順を「1つだけ」伝える
  2. 手順をやっているところを見せる
  3. 実際に手順をやってみる
    • 手順をやるときはそばでメンターがサポートをする
    • 手順をやるときは失敗しても大丈夫なOSSに対してやる

「1.」と「2.」は「選択肢を減らす」方針の実装です。1.で手順を伝え、2.で見本を見せ、「これを真似すればよい」ようにします。

「3.」の箇条書き2つは「安全な環境を用意する」方針の実装です。メンターがそばでサポートするので、自分ではうまく進められていなくても軌道修正できます。失敗しても大丈夫なOSSの開発に参加するので、メンターのサポートをすり抜けて失敗してしまっても大丈夫です。なお、失敗しても大丈夫なOSSとは「メンターが開発に関わっているOSS」です。開発者がその場にいるので失敗しても大丈夫だよ、というわけです。

「3.」は「実際に経験する」方針の実装です。↑の不安を排除する施策を実施した上で実行します。

OSSの開発に参加するオススメの手順

実際に紹介したOSSの開発に参加するオススメの手順を説明します。

それは「まず動かす」ことです。開発者としてではなくユーザーとして動かします。

開発するぞ!という意気込みで着手しようとするといきなり開発者として関わりたくなるものですが、そうではなく、まずはユーザーとして関わることがオススメです。

理由は、まずユーザーとして関わることで次のことがわかるからです。(逆に言うと、次のことをわかるためにユーザーとして動かします。)

  • 何をするソフトウェアなのか
  • どうやって使うか

これがわかったかどうかは「他の人に教える」ことで確認できます。他の人に何をするソフトウェアか、どうやって使うかを教えて、その人がそのソフトウェアのことをわかって使えるようになったら自分がわかっているということです。

さて、これらのことがわかるためには次のことをする必要があります。

  1. 「このOSSについて」のようなドキュメントを読む
  2. そのOSSをインストールする
  3. インストールしたOSSを動かしながらチュートリアルを実行する

多くのOSSでは、これらをする中でなにかしらつまづきます。

今までOSSの開発に参加していなかった人は、つまづいたら次のような行動をとります。

  • グチる
  • 使うことをやめる
  • Webを検索して解決策を見つけて解決し、先に進む

OSSの開発に参加するオススメの手順では違う行動をとります。つまづいたところをメモに残します。(今回のイベントではGitHubのissueを使いました。つまづいたところ1つに対して1つのissueです。)

メモに残すことは「直す」あるいは「報告する」ためです。「直す」も「報告する」もOSSの開発に参加していることになります。つまり、自分がつまづいたところを「OSSの開発に参加するチャンス!」だと捉えて行動するということです。これまで「OSSの開発に参加するきっかけなんてないよ…」と思っていた人も、見方を変えるだけで実はきっかけはたくさんあったことに気づきます。

グチったり、使うことをやめたり、回避策を見つけて先に進む代わりに、「次の人が同じようにつまづかないように」します。それが「直す」あるいは「報告する」です。

この時点では「直す・報告する」対象はコードではなくドキュメントのことが多いでしょう。それでいいのです。ドキュメントの改良も大事なOSSの開発です。ドキュメントの書き方でつまづかなくなる人がいるのですから。自分のちょっとした行動で、次の人がつまづかなくなるってステキなことじゃないですか。

一通り動かして、このOSSのことがわかったら、溜まっていたメモを元にupstream(開発元)にフィードバックします。

フィードバック方法については、次のような例を示し、個別のケースについてはメンターのみなさんにお任せしました。(もっと具体的な手順を示した方がよかったかもしれない。)

  • フィードバック時には次の情報を含める(「バグレポートに必要な情報」が指標)
    • 何をしたか
    • 期待した結果はなにか
    • 実際の結果はどうだったか
  • ↑の情報をまとめるときは次の手順でまとめる
    1. まずは自分がわかるようにまとめる
    2. 自分がわかるようにまとめたものを、開発者に伝わるようにまとめる(開発者にとってリーダブルなようにまとめる)

なお、オススメの手順を説明した資料は次の資料です。

結果

ほとんどの参加者が漠然とした不安を払拭できたようです。(イベントの最後に全員に挙手してもらって確認しました。)

この内容は今回のイベントのために考え、初めて実行したものです。今回の結果だけを考慮しただけですが、「漠然とした不安」を解消するやり方として、「オススメの手順を1つだけ示し」、「安全だと感じれる環境」で、「実際に経験する」というのは有効なやり方と言えそうです。

機会があれば、同様のアプローチで「OSSの開発に参加する人を増やす」ことを試してみたいです。(OSSの開発に参加する人を増やしたい方はご相談ください。)

よかったこと

今回のイベントでよかったことおよび次も続けたいことをまとめます。

  • メンターが楽しそうだったこと
    • 楽しいポイントがどこだったのかはわかりませんが、楽しかったのは非常によいことです。(参加者にも「楽しんでOSSの開発に参加して欲しい」と伝えました。)
  • メンターが参加者の隣に座れるイスがあったこと
    • イスに座ってサポートするとより深くサポートできるのではないかという気がしました。
    • pixivさんが提供してくれた会場には脇にたくさんイスがあり、すぐにイスを追加できて便利でした。

課題と解決案

今回の内容は、優秀なメンターがいたから成り立っていました。そのため、同様の内容を再現することが難しいのが課題です。

また、失敗しても大丈夫なOSS(メンターが開発者なOSS)は必ずしも題材として適切ではありません。たとえば、難易度が難しいとか必要な前提知識が多すぎると適切ではありません。適切な題材となりえるOSSを用意できるかどうかということも課題です。

解決案は…今のところないです。どうするといいでしょうねぇ。

まとめ

今回初めて開催したOSS Hack 4 Beginnersについて内容を考えた立場から紹介しました。OSS Hack 4 Beginnersは続くOSS Hack Weekendのためのイベントでした。OSS Hack Weekendでのびのびと楽しくOSSの開発に参加するために漠然とした不安を解消したのです。

なお、OSS Hack WeedendはOSS Hack 4 Beginnersに参加していなくても参加できます。引き続き優秀なメンターが多数参加するので、OSSの開発に参加したそうな学生さんに教えてあげてください。OSS Hack Weekendでは学生が希望するOSSの開発に参加します。OSS Hack 4 Beginnersはイベント開催側が対象OSSを選びましたが、OSS Hack Weekendでは野生のOSSの開発に参加します。「このOSSの開発に参加してみなよ」という案を持っている方はそれも一緒に学生に教えてあげてください。

  • OSS Hack Weekend:2015-07-11と07-12の2日間開催(07-06申し込み締め切り)

今回の勉強会に参加したメンターの感想に興味のある方は次のWebページを見てみてください。

2015-07-02

自己流JavaScriptを書いていた人がAngularJSのユニットテストで躓いた点

結城です。

最近、AngularJSを使ったWebアプリ開発のプロジェクトに参加する事になり、とりあえず一通りの事は把握しておかなければと思って公式のチュートリアル(英語)を実践してみたのですが、JavaScriptの経験が浅い人だとハマらなさそうだけれども、中途半端に経験があったせいでドハマり、という場面に遭遇してしまいました。 恥ずかしい話ですが、せっかくなので同じように躓いている人(もしいれば)のために、分かった事や理解のポイントを書き記しておこうと思います。

この記事の対象読者は、以下のような状況にある人です。

  • フレームワークを使わないJavaScript(例えば、jQueryを使ったJavaScript程度)は書いた事がある。
  • 自動テスト(特に、ユニットテスト)は書いた事がある。
  • AngularJSを始めたばかりである。
  • 依存性注入という概念は理解できるが、実際にどう使うかはあまり知らない。

依存性注入って何?

ソフトウェアの設計の話で、依存性注入(dependency injection、DI)という考え方があります。

例えば、Webアプリの開発においてサーバー(が提供するWeb API)との通信を行う場面はよくありますが、JavaScriptの世界で最も低レイヤのやり方として、XMLHttpRequestを使う方法があります。 XMLHttpRequestを使ってWeb APIと通信する事が前提のWebアプリは、「XMLHttpRequestというクラス」であったり「接続先のWeb API」であったりといった要素に依存していると言うことができます。

このようなWebアプリで問題になるのは、自動テストのしにくさです。 WebサーバやAPIサーバを用意して実際にWeb APIを提供するのは骨が折れますし、テストの実行ごとにまっさらな環境を整え直すというのも難しいです。

そこで登場するのが依存性注入、略してDIという考え方です。

例えば上記のようなWebアプリでは、「接続先のWeb APIのURL」を設定で変えられるようにしておけば、本物のWeb APIを用意しなくてもダミーのAPIサーバで動作テストが可能です。

また、XMLHttpRequestを使う処理を共通化してhttp()のような名前のユーティリティ関数として括り出しておき、Web APIとの通信は全てこの関数を使うようにしておけば、

  • 本番環境では、実際にWeb APIとの通信を行う。
  • 動作テストの時は、http()というユーティリティ関数の定義を置き換えて、実際の通信を行わずに、あらかじめ与えられた固定のJSON文字列を、あたかも実際の通信結果がそれであったかのような形で返すようにする。

という具合で、http()の置き換えだけで自動テストを簡単に行えるようになります。

このように、「本体の処理が依存している外部との接続箇所を変更可能にしておいて、本番環境とテストの時のように場面に応じて実装を切り替える」という設計のパターンをDIと言います。 DIという言葉は知らなくても、そういう設計のコードを書いた事があるという人は少なくないでしょう。

AngularJSでのDI

上記チュートリアルのステップ5ではXMLHttpRequestを使った通信と依存性注入の事について触れられています。

AngularJSでは、XMLHttpRequestをそのまま使うのではなく、AngularJSが提供している$httpというモジュール(正確には、このモジュールが内部で使用している、$httpBackendという別のモジュール)の機能を通じてWeb APIと通信する事が強く推奨されています。 同じ$httpという名前のモジュールであっても、実際に機能を使う時にはAngularJSというフレームワークによって

  • 本番運用では、実際のWeb APIと通信する。
  • 自動テストでは、あらかじめ用意しておいたダミーのレスポンスを使う。

という具合に挙動が変化する(自動的に実装が切り替わる)ため、自動テストをしやすくなっています。 「実際に通信する処理の切り替えはどうやってやるのか?」は、「フレームワークのユーザー」であるWebアプリ開発者は気にする必要はなく、単に$httpというモジュールを使うことを宣言するだけでOKです。

その宣言の仕方として、AngularJSではinject(function(モジュール1, モジュール2, ...) { 実行したい処理 })という書き方をすることになっています。 このように書くと、AngularJSはinject()に渡された関数の引数に書かれているモジュール1モジュール2といった名前のモジュールを探してきて、それらを実際に引数として渡して、その関数を実行します。

もっと平たく言うと、inject(function(モジュール1, モジュール2, ...) { 実行したい処理 })という書き方をすれば、「実行したい処理」の中でモジュール1モジュール2を利用できるようになります。 これが、AngularJSでのDIの基本です。

既存のフレームワークを使わずにDI的な事をやろうとすると、「どうやって依存モジュールを渡すのか」や「どうやって実装を切り替えるのか」といった点が悩み所になりますが、AngularJSでは「使いたいモジュールは関数の引数に書いておけばそれでいい。モジュールの実装は、フレームワークがその引数名に対応する物を勝手に探してくる。」という割り切りをすることで、Webアプリ開発者は極力何も考えなくていいようになっているわけです。よく考えられていますね。

AngularJSのユニットテストでのDI

このinject()を使ったDIの仕組みはAngularJS全体で共通して利用できるようになっていて、自動テストも例外ではありません。 例えば、上記チュートリアルのステップ5でも、ユニットテストのbeforeEach()(前処理の定義)を以下のように記述しています。

describe('PhoneListCtrl', function(){
  ...
  beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
    $httpBackend = _$httpBackend_;
    $httpBackend.expectGET('phones/phones.json').
        respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);

    scope = $rootScope.$new();
    ctrl = $controller('PhoneListCtrl', {$scope: scope});
  }));
...

……という所まで読み進めた時点で、自分は躓いてしまいました。 _$httpBackend_って何なんだ!? と。

実のところ、自分はその時点では「AngularJSでのDI」の全貌をまだきちんとは理解できておらず、かろうじて、最初に述べた「DIの一般的な話」だけを理解できていました。 その時の認識で上記のコード片を見ると、なんとなく、以下のように読めてしまっていました。

  • 本番用の$httpBackendの実装と、テスト時用の$httpBackendの実装を切り替える必要がある。
  • その切り替えは、_$httpBackend_という書き方で行う。前後にアンダースコアが付いた名前で参照すると、テスト時用のダミー実装が渡される。

ところが、どれだけ探してもこのアンダースコア付きの名前での参照についての詳しい話が見つかりません。 そのため、このコードが意味している所を理解できず、チュートリアルの次のステップに進めずにいました。 (最初はとりあえず読み飛ばして次に進んでみたのですが、後でまたこの書き方が出てきたので、頻出する書き方なのであれば理解が怪しいまま進むのは危険だと感じて、そこで止まってしまいました。)

「同じ動作をする、別の書き方」で理解する

理解の糸口となったのは、実際に動くコードが手元にあった事でした。

AngularJSのチュートリアルでは、そのステップで動かす事になっているコードそのもののリポジトリ(各stepがタグになっていて、タグをチェックアウトすればそのstepでのコード全体を見られる)が公開されていて、手元にそれをcloneして実際に動かしながらAngularJSを理解できるようになっています。 そのリポジトリでstep5の内容を見てみると、以下のようになっていました。

describe('PhoneListCtrl', function(){
  ...
  beforeEach(module('phonecatApp'));
  beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
    $httpBackend = _$httpBackend_;
    $httpBackend.expectGET('phones/phones.json').
        respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
    ...
  }));

  it('should create "phones" model with 2 phones fetched from xhr', function() {
    ...
    $httpBackend.flush();
    ...
  });
  ...

これを見てまず疑問に思ったのが、「inject()は必ず使わないといけないものじゃなかったのか?」という事でした。 というのも、チュートリアルで出てきたそれまでのコードでは基本的にinject()を使っていたので、使っているコードと使っていないコードが混在しうるというのがまず驚きでした。

次に思ったのは、「これを必ずinject()を使うようにして書く事もできるのだろうか?」という事でした。 それでコードを書き換えながら実行していたところ、以下のように書き変えても結果は変わらないという事が分かりました。

describe('PhoneListCtrl', function(){
  ...
  beforeEach(inject(function($httpBackend, $rootScope, $controller) {
    $httpBackend.expectGET('phones/phones.json').
        respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
    ...
  }));

  it('should create "phones" model with 2 phones fetched from xhr',
     inject(function($httpBackend) {
    ...
    $httpBackend.flush();
    ...
  }));
  ...

また、以下のように書き換えても同様でした。

describe('PhoneListCtrl', function(){
  var backend;
  ...
  beforeEach(inject(function($httpBackend, $rootScope, $controller) {
    backend = $httpBackend;
    backend.expectGET('phones/phones.json').
        respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
    ...
  }));

  it('should create "phones" model with 2 phones fetched from xhr', function() {
    ...
    backend.flush();
    ...
  });
  ...

これを踏まえて最初のコードのコメントを改めて読み直してみた所、ようやく意味が分かりました。

  // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
  // This allows us to inject a service but then attach it to a variable
  // with the same name as the service in order to avoid a name conflict.

このコメントでは「injectorはモジュール名の前後に付けられたアンダースコアを無視する」と書かれていますが、自分はこれを深読みしすぎて、実装の切り替えなどの話と絡めて考えてしまっていました。 そうではなく、これは本当に文字通りに、「_$httpBackend_と書いても、$httpBackendと書いたのと同じに扱われる」という意味なのでした。

つまり、こういう事です。 一切の省略をしないでDIを素直に使って自動テストを書くと、

describe('PhoneListCtrl', function(){
  ...
  beforeEach(inject(function($httpBackend, $rootScope, $controller) {
    $httpBackend.expectGET('phones/phones.json').
        respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
    ...
  }));

  it('should create "phones" model with 2 phones fetched from xhr',
     inject(function($httpBackend) {
    ...
    $httpBackend.flush();
    ...
  }));
  ...

という風に、$httpBackendというモジュールをinject()する書き方を何度も何度も書かないといけません。

しかし、テストの数が増えてくると、何度も何度もinject()するのはいかにも冗長です。 そういう時に、フレームワークをあまり使わない・使った事がない人が思いつくのが、beforeEach()で一時的な変数に1回だけ代入するというやり方でしょう。

describe('PhoneListCtrl', function(){
  var backend;
  ...
  beforeEach(inject(function($httpBackend, $rootScope, $controller) {
    backend = $httpBackend;
    backend.expectGET('phones/phones.json').
        respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
    ...
  }));

  it('should create "phones" model with 2 phones fetched from xhr', function() {
    ...
    backend.flush();
    ...
  });
  ...

AngularJSのチュートリアルに含まれているコードに書かれている内容も実質的にはこれと同じなのですが、こちらのやり方には1つ問題があります。 それは、依存しているモジュールをどの名前で参照すればいいのかが分かりにくくなるという事です。 この例であればbackendという変数名を使っていますが、人によってはHTTP Backendの略でhbと名付けるかも知れませんし、あるいはアンダースコアを付けて_$httpBackendとしたり、ダラーを重ねて$$httpBackendとしたりするかも知れません。 1つの開発プロジェクトの中で、その時の気分や担当者によって同じ物をどう書くかの書き方がばらけてしまうと、複数人での共同作業や長期的なメンテナンスの際に、混乱の元になってしまいます。

これと似たような事象が、低レベルのJavaScriptを書く場面でもよくあります。 それは、thisにどのような別名を割り当てるかという場面です。

JavaScriptでは、thisが指す対象は関数の実行時の文脈によって変動するにも関わらず、コールバック関数という形で処理の一部を分けて書く場面が多発します。 そのため、Rubyなどの他の言語の感覚でコードを書くとエラーになる事があります。 例えば以下のようなコードです。

var oldValue = this.currentValue;
setTimeout(function() {
  var newValue = this.currentValue;
  // ↑ここでは this === undefined になっているのでエラーになる
  console.log('delta: ' + (newValue - oldValue));
}, 1000);

Function.prototype.bind()を利用できる状況では、以下のように書けば問題ありません。

var oldValue = this.currentValue;
setTimeout((function() {
  var newValue = this.currentValue;
  // ↑束縛されたthisなので、これは1行目のthisと同じ物。
  console.log('delta: ' + (newValue - oldValue));
}).bind(this) /* ←ここでthisを束縛 */, 1000);

Function.prototype.bind()を使えない場合は、以下のように書くのが一般的です。

var oldValue = this.currentValue;
var self = this; // ←ここで別名を割り当てている
setTimeout(function() {
  var newValue = self.currentValue;
  // ↑別名で参照しているだけで、これは1行目のthisと同じ物。
  console.log('delta: ' + (newValue - oldValue));
}, 1000);

この例ではselfという変数名でthisの内容を参照できるようにしていますが、この時使う変数名はthat_thisなど様々な流儀があります(自分はselfを使う事が多いです)。 そのため、他の人の書いたコードや他のプロジェクトのコードで違う流儀の物を見ると、コードが意味する内容を読み取るのに時間がかかったり、意味を誤解してしまったりする事があります。

AngularJSでは、この発想を逆転することで、このような混乱した状況の発生を予防していると言えます。 つまり、最初から、$httpBackendという名前のモジュールを_$httpBackend_という別名でも参照できるようにしてあるため、$httpBackendという本来のモジュール名を「複数の関数で参照するための一時的な別名」として使えるのです。 この「最初から使える別名」のおかげで、AngularJSでは「一時的な別名をどう決めればいいか?」という事に無駄に悩まなくてもいいようになっているわけです。

まとめ

自分はJavaScriptを使うプロダクトをフレームワークを使わずに開発していた時期が長かったため、上記の「thisの別名問題」のようなやり方がすっかり身に染みついてしまっていました。 そのため、同じ物を敢えて別の名前で参照する(できるようにしてある)という発想が無く、思わぬ所で理解に躓いてしまいました。 自分自身が知らず知らずのうちに固定観念に囚われてしまっていたことを意識させられる出来事でした。

また、「Webアプリケーションフレームワーク」と同じようにメタ的な存在である「テスティングフレームワーク」を開発する中で悩み所になりがちだった問題について、AngularJSでは利用者目線での使い勝手を優先して巧みな解決策を用意している事が分かりました。 自分が今後フレームワークを開発する際にも、「フレームワークの実装のしやすさ」だけに囚われず、フレームワーク利用者の利便性を最大化するための努力を惜しまないように気を付けたい、という思いを新たにした次第です。

オチは特にありません。

タグ: JavaScript
2015-07-10

CutterのGStreamerサポートについて

はじめに

C/C++に対応しているテスティングフレームワークの一つにCutterがあります。

今回はCutterのGStreamerサポートについて紹介します。Cutterの特徴について知りたい方はCutterの機能を参照してください。 Cutterそのものについては過去に何度かククログでもとりあげていますので、そちらも併せて参照するとよいでしょう。

CutterのGStreamerサポートとは

CutterのGStreamerサポートと聞いて、何を思い浮かべるでしょうか。

  • CutterでGStreamerを使っているアプリケーションやライブラリのテストを書きやすくする仕組みのこと
  • GStreamerの仕組みを使ってテストを実行すること

前者のことと思ったかも知れません。正解は後者の「GStreamerの仕組みを使ってテストを実行すること」です。

GStreamerを使ってテストを実行するには

では、実際に実行してみましょう。そのためには、この記事を書いている時点ではまだリリースされていないCutterの最新版を使う必要があります。GStreamer 1.0に対応したのがごく最近であるためです。

Cutterの最新版をビルドする

GitHubからソースコードを入手してビルドをはじめるために、次のコマンドを実行します。

% git clone https://github.com/clear-code/cutter.git
% cd cutter
% ./autogen.sh
% ./configure

ここで、 configure の結果で「GStreamer : yes」が含まれていることを確認します。ここで yes になっていないとGStreamerサポートが有効になりません。GStreamerの開発パッケージがインストール済であるか確認してください。 Debian系のディストリビューションでは、あらかじめ libgstreamer1.0-dev をインストールしておく必要があります。

Libraries:
  ...
  GStreamer                        : yes
  ...

うまくGStreamerを検出できていると、次のようにプラグインのディレクトリも正しいものが表示されます。

  GStreamer plugins directory      : /usr/lib/x86_64-linux-gnu/gstreamer-1.0

ここまできたら、あとは make を実行するだけです。

% make
テストを実行する

Cutterをビルドできたので、テストを実行してみましょう。 *1

cloneしたソースコードに gst-plugins というディレクトリがあるのでそちらに移動します。

% cd gst-plugins

すると、いくつかサンプルのシェルスクリプトがあるのがわかります。

% ls -la *.sh
-rwxr-xr-x 1 kenhys kenhys 584  7月 11 01:52 run-client.sh
-rwxr-xr-x 1 kenhys kenhys 580  7月 11 01:52 run-server.sh
-rwxr-xr-x 1 kenhys kenhys 679  7月 11 01:52 run-test.sh

ここでは、一番簡単な run-test.sh の内容を見てみましょう。

% cat run-test.sh
#!/bin/sh

export BASE_DIR="`dirname $0`"

if test x"$NO_MAKE" != x"yes"; then
    make -C $BASE_DIR/../ > /dev/null || exit 1
fi

export CUT_UI_MODULE_DIR=$BASE_DIR/../module/ui/.libs
export CUT_UI_FACTORY_MODULE_DIR=$BASE_DIR/../module/ui/.libs
export CUT_REPORT_MODULE_DIR=$BASE_DIR/../module/report/.libs
export CUT_REPORT_FACTORY_MODULE_DIR=$BASE_DIR/../module/report/.libs
export CUT_STREAM_MODULE_DIR=$BASE_DIR/../module/stream/.libs
export CUT_STREAM_FACTORY_MODULE_DIR=$BASE_DIR/../module/stream/.libs

export GST_PLUGIN_PATH=$BASE_DIR/.libs
gst-launch-1.0 \
  cutter-test-runner test-directory=$BASE_DIR/test ! \
  cutter-console-output verbose-level=v use-color=true

ポイントは最後の行で gst-launch-1.0 を使っているところです。

gst-launch-1.0 \
  cutter-test-runner test-directory=$BASE_DIR/test ! \
  cutter-console-output verbose-level=v use-color=true

Cutter特有のエレメントである、「cutter-test-runner」や、「cutter-console-output」を指定してパイプラインを組み立てているのがわかります。 *2

では、run-test.sh を実行してみましょう。

% ./run-test.sh
パイプラインを一時停止 (PAUSED) にしています...
Pipeline is PREROLLING ...
Pipeline is PREROLLED ...
パイプラインを再生中 (PLAYING) にしています...
New clock: GstSystemClock
dummy_loader_test:
  test_dummy_function2:                                 .: (0.000001)
  test_dummy_function3:                                 .: (0.000001)
  test_dummy_function1:                                 .: (0.000000)
  test_abcdefghijklmnopqratuvwzyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:.: (0.000000)

Finished in 0.000935 seconds (total: 0.000002 seconds)

4 test(s), 0 assertion(s), 0 failure(s), 0 error(s), 0 pending(s), 0 omission(s), 0 notification(s)
100% passed
Got EOS from element "pipeline0".
Execution ended after 0:00:00.000993224
パイプラインを一時停止 (PAUSED) にしています...
Setting pipeline to READY ...
Setting pipeline to NULL ...
Freeing pipeline ...

GStreamerでよくみるメッセージとともに、テストの実行結果が表示されているのがわかります。

この例ではテストを実行するのも、結果を表示するのも同一PCで行っています。

テストの実行と結果を別の環境で行う

先程の例では、テストの実行と結果を表示するのは同一のPCにて行っていました。今度はそれを、別の環境で行ってみましょう。 GStreamerに付属の tcpserversrctcpclientsink といったエレメントを使って実現できます。

テストを実行する側では tcpclientsink を指定し、テスト結果を受けとって表示する側では tcpserversrc を指定して TCPによるテスト結果の送受信をするのがポイントです。

gst-launch-1.0 のパイプライン指定はそれぞれ次のようになります。

テスト実施側のコマンドライン:

% gst-launch-1.0 \
    cutter-test-runner test-directory=$BASE_DIR/test ! \
    tcpclientsink host=(テスト結果受信側のIP) port=50000

テスト結果の受信側のコマンドライン:

% gst-launch-1.0 \
    tcpserversrc host=(テスト結果受信側のIP) port=50000 ! \
    cutter-console-output verbose-level=v use-color=true

簡単に実行するために、テスト実施側用として run-server.sh があります。また、テスト結果の受信側用に run-client.sh があります。

それぞれのスクリプトを実行する順番は次の通りです。

  • run-client.sh をテスト結果受信側で実行する
  • run-server.sh をテスト実施側で実行する
$ ./run-client.sh 
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...
Pipeline is PREROLLED ...
Setting pipeline to PLAYING ...
New clock: GstSystemClock
dummy_loader_test:
  test_dummy_function2:                                 .: (0.000000)
  test_dummy_function3:                                 .: (0.000000)
  test_dummy_function1:                                 .: (0.000001)
  test_abcdefghijklmnopqratuvwzyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:.: (0.000001)

Finished in 0.003666 seconds (total: 0.000002 seconds)

4 test(s), 0 assertion(s), 0 failure(s), 0 error(s), 0 pending(s), 0 omission(s), 0 notification(s)
100% passed
Got EOS from element "pipeline0".
Execution ended after 0:00:00.005115664
Setting pipeline to PAUSED ...
Setting pipeline to READY ...
Setting pipeline to NULL ...
Freeing pipeline ...

これで、テストの実行環境とは別の環境で結果を表示することができました。 リモートで実行中のCutterのテスト結果のログを手元に保存する、というのが簡単にできるようになります。

まとめ

今回はCutterというテスティングフレームワークのGStreamerサポートについて紹介しました。

Cutterにはほかにもテスト環境を便利にする機能があります。まだCutterを使ったことがない人はチュートリアルからはじめるとよいでしょう。詳しく知りたい人はリファレンスマニュアルを参照してください。

*1 付属のサンプルを実行するだけなので、make installは実行していない。

*2 エレメントの詳細は gst-inspect-1.0 で確認できる。

タグ: Cutter
2015-07-13

SEゼミ2015のコンテンツ作成と進行をしてわかったこと #sezemi

注意:長いです。全体としてはキレイにまとまりませんでした。(途中で雰囲気が変わる。)個々の話題で気になるところだけ参照するくらいがよいかもしれません。

前提

2015年6月6日から2015年7月12日の間に2015年のSEゼミが開催されました。SEゼミは学生と企業のマッチングを支援する企画で、複数の学生向けイベントで構成されています。クリアコードはソフトウェア開発とフリーソフトウェアのサポートが主な仕事内容で、学生と企業のマッチングの仕事はしていませんし、イベント開催や講演も機会があったり必要に迫られるとやる、くらいです。

そんなクリアコードですが、2015年のSEゼミでは次のように3/4のイベントのコンテンツ作成とイベント当日の進行をしました。

  1. リーダブルコード勉強会
    • 2015年6月6日開催 - コンテンツ作成・進行
  2. GitHub勉強会
    • 2015年6月20日開催 - 関わっていない
  3. OSS Hack 4 Beginners
    • 2015年6月27日開催 - コンテンツ作成・進行
  4. OSS Hack Weekend
    • 2015年7月11日、12日開催 - コンテンツ作成・進行

なぜコンテンツ作成と進行をしたかというと、SEゼミを企画しているSEプラスさんから「今年はOSS開発をテーマにしたいのです。協力してもらえませんか?」と打診を受けたからです。クリアコードが大事にしていることの1つは「ユーザーが自由に使えるソフトウェア」です。SEゼミの企画を通じてOSSの開発に参加する人が増え、さらに、OSSの開発に参加しているという活動が評価される社会に近づくことは、クリアコードが大事にしていることに通じます。そのためSEゼミをお手伝いすることにしました。

このように、普段はイベント開催やセミナー、学生向けのなにかなどをしていないソフトウェア開発の会社が学生向けのOSS開発をテーマにしたイベントのコンテンツを作成・進行する中で得られた知見を記録しておきます。もしかしたら、同様のイベントを開催するだれかの参考になるかもしれません。それぞれの知見は関連していないことが多いので、部分的に参考にすることもできるでしょう。

方針

SEプラスさんから依頼を受けたときに、一緒に方針を検討しました。

普段は学生との接点もないですし、学生と企業のマッチングの経験もありません。クリアコードの採用情報ページを見てもらうとわかりますが、学生向けのなにかを用意したりしていませんし、世間での一般的な採用プロセスもやっていないので、採用に関するノウハウはありません。そのため、セオリー通り(?あるのかどうかもわからない位の知識ですが…)のマッチングイベントのコンテンツにすることは諦めました。

そうではなく、学生も企業もSEプラスさんもクリアコードもうれしくなるコンテンツにできないか考えました。そのため、まずはそれぞれがどうなるとうれしくなるか考えました。

  • 学生 - 自分が興味のある(必要だと思う)知識が増える。
  • 企業 - 一緒に働きたい学生と接点ができる。
  • SEプラスさん - 学生と企業のマッチングの「よい」機会を提供する。
  • クリアコード - OSSの開発に参加する人が増える。OSSの開発が評価される社会に近づく。

これだけ見るとみんながうれしくなりそうには見えません。

学生は就職に興味があるかわからないので、企業との接点がうれしいかどうかはわかりません。

企業はどうやって「一緒に働きたいか」判断すればよいのかわかりません。

学生と企業のマッチングの機会を提供しただけで、マッチする人はほとんどいませんでした、となってはSEプラスさんもうれしくありません。

そこで出てきたのが次のアイディアです。

学生がOSSの開発に参加することを段階的にサポートする。サポートするのは企業の現役エンジニア。

学生に「OSSの開発に参加しよう!」と言っても敷居が高く感じられて集まらないだろう。そうすると企業は学生と接点を持てない。それならば、OSSの開発に必要なことを順に学べるようにすれば敷居を下げられるのではないか。

企業でOSSを使うことは当たり前になり、企業にとって重要なところにOSSを使っているケースも増えている。今はまだOSSの開発に参加していないとしても、「OSSの開発に参加しよう」という気持ちを持って学ぼうとしている学生は企業にとって「一緒に働きたい」可能性が高いのではないか。

ただ、企業としては、将来性を含めて一緒に働きたい学生を見極めることは難しい。どうすればよいか。企業のエンジニアが学生と一緒に開発をし、考え方や問題にぶつかった時の対応、アドバイスを受けた時の反応などをみることができれば、それらはよい判断材料になるのではないか。

OSSの開発に参加する学生が増えればクリアコードはうれしい。

よし、学生と企業のエンジニアがOSSを一緒に開発するコンテンツにしよう。これなら、みんながうれしくなりそう。

ということで、「学生がOSSの開発に参加することを段階的にサポートする。サポートするのは企業の現役エンジニア。」という方針にしました。この方針だと次のようにみんながうれしくなりそうだからです。

  • 学生 - 自分が興味のある(必要だと思う)知識が増える。
    • → 少しでもOSSの開発に興味がありそうな学生ならOSSの開発に参加できるようになる。
  • 企業 - 一緒に働きたい学生と接点ができる。
    • → OSSを使っている企業ならOSSの開発に興味がある学生は一緒に働きたい人である可能性が高そう。一緒に開発することで必要な判断材料を集められそう。
  • SEプラスさん - 学生と企業のマッチングの「よい」機会を提供する。
    • → OSSを軸にした「よい」機会を提供できそう。
  • クリアコード - OSSの開発に参加する人が増える。OSSの開発が評価される社会に近づく。
    • → OSSの開発に参加する学生が増えそう。企業がOSSの開発に参加していることを評価することにつながればもっとOSSの開発に参加する人が増えそう。

この方針に沿ってコンテンツを決めました。

コンテンツの作成とイベント当日の進行をすることでいろいろ知見が得られたので紹介します。

コンテンツの質の違い

「OSSの開発に参加する」というOSS Hack Weekendというイベントの前に、OSSの開発に必要な知識を提供する次の3つのイベントを開催することにしました。最初にSEプラスさんと相談していた時点では、このくらいのざっくりとしたことしか決めていませんでした。

  • リーダブルコード勉強会 - OSSを改良・修正したコードをプロジェクトに送るときに「開発者」にとってリーダブルなコードを書けるとよさそう。
  • GitHub勉強会 - 最近はGitHubを使って開発しているOSSが多いので、GitHubの使い方を知っておくとよさそう。
  • OSS Hack 4 Beginners - OSSの開発に参加したことがない人に「参加方法」を教えたほうがよさそう。

イベントページに載っているタイムテーブルはこの頃のざっくりをベースにしたもので、最終的なタイムテーブルは少し違うものになっています。(GitHub勉強会については関わっていないのでわかりません。これ以降の話は、GitHub勉強会以外の話です。)

ただ、目指す方向はこの頃と変わっていません。OSS Hack Weekendも方向は変わっていません。

このときは意識していなかったのですが、イベントのコンテンツの「質」は事前のイベントとOSS Hack Weekendで違いました。事前のイベントは「みんなが同じ課題を使って学ぶ」コンテンツで、OSS Hack Weekendは「それぞれが別の課題に取り組む」コンテンツです。今回はやってみるまでこの違いの影響がわかりませんでした。やってみて初めてやり方を変えた方がよいことに気づきました。

やり方の変え方のポイントは「全員に説明が届く」ことを前提とするかどうかです。

「みんなが同じ課題を使って学ぶ」コンテンツ

「みんなが同じ課題を使って学ぶ」コンテンツは全員が同じタイミングで同じ作業をします。それぞれで多少のズレはありますが、同じと考えて問題ないレベルです。

このコンテンツの場合はある程度「全員に説明が届く」ことを前提とすることができます。

「全員に説明が届く」ということは、進行しながら次のことを実現できるということです。

  • 次に何をやるか細かく区切って説明できる。
    • 課題としてA→B→Cをやるときに「まずAをやりましょう。やり方は○○です。どうぞ。…みんな終わりましたね。では次はBをやります。やり方は○○です。どうぞ。…」ということができるということです。
  • なぜこれをやるとよいかという背景やねらいをていねいに説明できる。
    • 全員の状態が同じなので「みんなが今やった課題は○○という理由でよいからやりました。次の課題の△△は××という理由でよいのでやってみましょう。」と説明できるということです。状態がずれていると抽象的な話や特定の人向けの話しかできません。

↑は学生向けの話ですが、最初の「次に何をやるか細かく区切って説明できる」はメンター向けにも当てはまりました。

今回は、メンターが20人から30人もいました。数人なら事前に進め方や大事なことを具体的に共有して、イベントの最中は学生向けの説明だけにする(必要なら個別にメンターに伝える)、ということもできます。しかし、20人から30人いるとそのようなやり方はできません。

そのため、メンターのうち、事前に伝えられる人には「方向性」だけを伝えて後はいい感じに動いてください、詳細はイベントの最中に学生向けの説明と一緒に説明します、というやり方にしました。「全員に説明が届く」ならこのやり方でもうまくいきました。

なお、「方向性」は「答えを教えない。考え方を伝えながら一緒にやる。」でした。これは、学生が考える機会を奪わない・考え方を知った方が後でもっと役に立つという学生のためでもありますが、学生が考えるところに立ち会うことがメンターの判断材料になるのではないかという期待からでした。

一部のメンターには想定外のよい効果があったようです。自分が無意識のうちにやっていることを言語化するきっかけになる、ということです。

「それぞれが別の課題に取り組む」コンテンツ

「それぞれが別の課題に取り組む」コンテンツでは最初と最後以外「全員に説明が届く」ことを前提とすることができません。

イベントの途中ではそれぞれで取り組んでいることも違いますし、直面している課題も違います。自分の作業に区切りがついている人もいれば、そうではない人もいます。つまり、次のような状態になります。

  • 全員にとって有用そうな内容は薄っぺらいものしかない。
  • 区切りがついていない人は説明を聞くより作業を続けたい。

これに気づいていなかったので、OSS Hack Weekendではよく知らないプログラムの直し方を説明してしまいました。この話は「今」問題を調査している人には有用な話題でしょうが、そうではない人(これから問題を調査する人も含む)には有用ではありません。「今」必要ではないからです。また、説明を聞く準備ができていない人(作業を進めたい人)もいます。(1日目でこれに気付いたので、2日目は「全員に有用そうな説明をする時間」をなくし、イベント進行だけにしました。)

そのため、「全員に説明が届く」ことを前提とできない場面では、説明はなるべく少なくして、その分、それぞれが課題に取り組む時間を増やした方が有効です。問題にぶつかったときに個別に説明する方がよっぽどその人に説明が届きます。

OSS Hack Weekendでは「ミニガイド」という聞きたい人だけメンターの説明を聞くタイミングがありました。「全員に説明が届く」ことを前提とできないケースでは、個別の説明とミニガイドの組み合わせは有用でしょう。ただし、個別の説明をするためには今回のように参加者数に対してメンター数がそれなりの人数(今回は参加者約40名に対してメンター約30名)いないと成り立たないでしょう。

なお、説明が届かないのはメンターにも同じことが言えます。そのため、イベントを進行しながらメンターに進め方を説明しなくても進められる方法が必要です。

今回はイベントの内容をシナリオとして事前に文書化していたのですが、それを共有して随時参照してもらうのがよいかもしれません。ただし、今の内容よりもメンター向けの記述をもっと増やす必要があります。

不安を解消するためのコンテンツ

2015年のSEゼミを通じて新しい発見だったことの1つは「OSSの開発に参加していない理由は漠然とした不安があるから」です。「参加してみたいと思っているなら手を動かしてみればいいのになんでやらないんだろう」と不思議でしたが「漠然とした不安」があるという理由だとは予想外でした。

「OSSの開発に参加しよう!」というイベントを開催するときは「漠然とした不安」を解消するケアをするとよさそうです。なお、学生は「OSSの開発に参加しよう!」という響きで「敷居が高い」と感じるらしいので、その敷居を下げるケアもすると参加者は増えるかもしれません。ちなみに、今回のSEゼミでは響きの敷居を下げることはしませんでした。今回のSEゼミに参加した学生の中には、参加する前は「OSS」はなんの略だろう、オープンソース…最後のSはなんだろう、というくらいの認識だった学生もいました。そのくらいの事前知識の人が悩みながらも参加しようと思える、くらいの敷居が今回のSEゼミだったようです。

「漠然とした不安」を解消するために、「やり方を教える。練習場所を作る。やってみてもらう。」という方法が有効であることがわかりました。これは他のときでも使えそうです。

具体的にどうやったかはSEゼミ2015 - OSS Hack 4 Beginnersを開催を参照してください。

学生は素直で自分で考えて説明することができる

SEゼミに参加した学生が特別なのか、多くの学生がそうなのかはわかりませんが、少なくともSEゼミに参加した学生たちのうち、個別に話した学生はみな素直で、自分で考えることができ、さらに自分が考えていることを説明できる人たちでした。

どういうことかというと、メンターからの新しい考え方を受け入れ、それに基づいて目の前の課題について自分で考えて、その場でいま自分がどう考えているかを説明できる人たちばかりだったということです。

イベントでは毎回新しい考え方を伝えるようにしていました。全体向けでの説明でもそうですし、個別の説明でもそうです。そして、新しい考え方を伝えて、今の場合だとどうなる?と聞くと、すぐにその考え方に基づいて自分で考え、それを口にしてくれたのです。

これは、社会人でも難しいことだと認識していたのですが、SEゼミに参加した学生(のうち少なくとも個別に話した学生)はみなできていたのです。(将来が楽しみです。)

そのため、「新しい考えを抵抗なく理解できる」、「自分で考えられる」ことを前提のコンテンツにしても問題ないでしょう。

事前準備を手伝ってあげればあとは自分でOSSの開発に参加できる

OSS Hack Weekendでは「難易度の低そうなOSS」ではなく、「自分が開発に参加したいOSS」を開発対象にして開発に参加しました。ソースコードが多いOSSもあれば、難易度の高いOSSもありました。それでも、多くの参加者はOSSの開発に参加できていました。

事前のイベントで必要な知識を伝え、実際に経験したあとなら、「自分はできるわけない…」と思っていた学生も「自分が開発に参加したいOSS」の開発に参加できることがわかりました。未経験の学生がOSSの開発に参加する!というイベントは決して無謀なイベントではありません。うまくやれば成り立つイベントです。SEゼミで使った資料はだれでも自由に使えるように公開してあります。OSSと同じように資料を再利用することもできますし、資料から学ぶこともできます。参加者からのアンケート結果も公開しています。(例:OSS Hack Weekendのアンケート結果

注意点は「必要な知識」をきちんと伝えることです。例えば、「ドキュメントの改善も立派なコントリビュート」ですし、「コードのコントリビュートだから偉い、ドキュメントだからまだまだだね、ということはない」とかそういうことです。学生にインパクトのある印象を持ってもらうために「有名プロジェクトにコミット!」とか「インターネット上で激しい議論!」とか華々しい話ばかり伝えたくなるかもしれませんが、たとえ地味でも、実際に開発している人目線の地に足の着いた知識も伝えてあげてください。OSSの開発は小さなことの積み重ねがたくさんあって成り立っているのですから。

OSSの開発に関して伝える大事なことは「楽しむ」だけでよい

当初はOSSの開発に参加する理由に「実用的なメリットもあるよ」ということも入れていましたが、最終的(OSS Hack Weekendの2日目)には「楽しむ」だけにしました。楽しむことができたなら、そのあとどうするかは学生がそれぞれ判断できます。コンテンツ作成側は、学生が「自分が開発に参加したいOSS」を選んで、そのOSSの開発に参加することを支援する機会を作って、実際に開発を「楽しむ」ことが大事だと伝えるだけで十分でした。

実は、楽しく開発することはクリアコードが大事にしていることです。学生向けだからとムリに噛み砕こうとせずに、そのまま伝えるだけでよかったのでした。

外部から学生のモチベーションを操作しようとしなくても、実際に体験することを支援すれば、学生は自然と自分の意思で行動するようになるでしょう。

なお、「楽しむ」は学生だけでなく、メンターにとっても大事そうです。学生もメンターも「楽しむ」ことができるコンテンツを目指すとよさそうです。「答えを教えない」という「制限」を加えつつも「あとは自己判断でいい感じにお願いします!」と自由に動いてもらったり、「一緒に開発する」機会を増やすとメンターも楽しめるコンテンツになりそうです。

メンターを集めることは大変

今回は非常に多くの優秀なエンジニアがメンターとして参加しました。2015年のSEゼミでやったやり方は、より多くの学生を集める、という方向には向かえないやり方です。学生の数が増えるほどメンターの数も増える必要があるからです。

企業の方の何人かに「今回のイベントはよかったので、年に2回やりましょうよ」というフィードバックをもらいましたが、これだけのメンターを集めることは難しいため、今回のコンテンツを今回の規模で何度も開催というのは難しそうです。

ただ、多くの複数の企業のエンジニアが同じイベントでメンターをする機会は貴重なので、このようなスタイルが今回よりもカジュアルに実現できる世界は楽しそうです。

まとめ

2015年のSEゼミが終わったので、そこで得られた知見をまとめようと試みました。全体としてキレイにまとめようと試みましたが、全体のまとまり感は作れず、個別の知見を列挙する形になりました。

参加した学生のみなさんはこれからも楽しくOSSの開発に参加してください。メンターのみなさん、また機会があったら楽しく協力してください。

2015-07-14

GStreamer 0.10からGStreamer 1.0へ移行するには

はじめに

C/C++に対応しているテスティングフレームワークの一つにCutterがあります。

以前、CutterのGStreamerサポートについて というCutterのテストをGStreamerの仕組みと組み合わせて実行する方法についての記事を書きました。

今回は、Cutterで当初サポートしていた GStreamer 0.10系に代わり、GStreamer 1.0への移行をどのように行ったかについて紹介します。Cutterの特徴について知りたい方はCutterの機能を参照してください。 Cutterそのものについては過去に何度かククログでもとりあげていますので、そちらも併せて参照するとよいでしょう。

GStreamer 0.10時代の終わり

CutterにGStreamerのサポートがはいったのは、2008年のことでした。まだ、GStreamerのバージョンが 0.10.20ぐらいのときのことです。 その後、GStreamerやGLibのバージョンアップにともない動作しなくなったことから、Cutter 1.2.3ではGStreamer 0.10向けのサポートを打ち切りました。

そしてGStreamer 1.0へ

Cutter 1.2.3のリリース以降、GStreamer 0.10からGStreamer 1.0へのAPIの変更に追従できないまま *1 になっていましたが、最近になってようやくその対応が入りました。

Cutterが提供しているエレメントは、あくまでテストをGStreamerの枠組みで行うためのもので、映像や音声を扱う一般的なエレメントとは異なります。 そのため、API等の変更の影響はそれほどありませんでした。とはいえ細かな修正をいくつか実施する必要がありました。

例えば、以下のような変更が必要でした。

  • 廃止された定数の修正
  • 廃止されたエレメントの修正
  • 廃止されたマクロの修正
  • 廃止されたvirtual methodの修正
  • マクロ定義の変更への追従

それぞれ、どんな変更をしたのかを説明します。

なお、移行にあたっては、GStreamer 0.10 to 1.0 porting guideをまず最初に参照しました。

廃止された定数の修正

Cutterでは、廃止された GST_FLOW_UNEXPECTED を 使っていたため、同等の GST_FLOW_EOS へと置き換えました。

diff --git a/gst-plugins/gst-cutter-test-runner.c b/gst-plugins/gst-cutter-test-runner.c
index 8b1ccbc..bd38dcd 100644
--- a/gst-plugins/gst-cutter-test-runner.c
+++ b/gst-plugins/gst-cutter-test-runner.c
@@ -284,7 +284,7 @@ create (GstBaseSrc *base_src, guint64 offset,
     GST_BUFFER_OFFSET_END(buf) = offset + send_size;
     *buffer = buf;
 
-    return !is_end_of_buffer ? GST_FLOW_OK : GST_FLOW_UNEXPECTED;
+    return !is_end_of_buffer ? GST_FLOW_OK : GST_FLOW_EOS;
 }
 
 static GstStateChangeReturn
廃止されたエレメントの修正

Cutterでは、廃止された GstElementDetails を使っていました。 そのため、メタデータを設定するためのAPIである、 gst_element_class_set_metadata に置き換える必要がありました。

diff --git a/gst-plugins/gst-cutter-server.c b/gst-plugins/gst-cutter-server.c
index 40f4e8d..1723d22 100644
--- a/gst-plugins/gst-cutter-server.c
+++ b/gst-plugins/gst-cutter-server.c
@@ -30,11 +30,6 @@
 GST_DEBUG_CATEGORY_STATIC(cutter_server_debug);
 #define GST_CAT_DEFAULT cutter_server_debug
 
-static const GstElementDetails cutter_server_details =
-    GST_ELEMENT_DETAILS("Cutter test server",
-                        "Cutter test server",
-                        "Cutter test server",
-                        "g新部 Hiroyuki Ikezoe  <poincare@ikezoe.net>");
 static GstStaticPadTemplate cutter_server_src_template_factory =
     GST_STATIC_PAD_TEMPLATE("src",
                             GST_PAD_SRC,
@@ -97,7 +92,11 @@ gst_cutter_server_base_init (gpointer klass)
     gst_element_class_add_pad_template(element_class,
         gst_static_pad_template_get(&cutter_server_sink_template_factory));
 
-    gst_element_class_set_details(element_class, &cutter_server_details);
+    gst_element_class_set_metadata(element_class,
+                                   "Cutter test server",
+                                   "Cutter test server",
+                                   "Cutter test server",
+                                   "g新部 Hiroyuki Ikezoe  <poincare@ikezoe.net>");
 }
廃止されたマクロの修正

Cutterでは、廃止された GST_BUFFER_XXX 系のマクロを使っていました。そのため、その部分について修正する必要がありました。

例えば、プログラムの文脈に応じて GstBuffer を扱う上でより適切と思われるAPIである gst_buffer_map を使う、gst_buffer_fill に置き換える、などです。

また、GST_BOILERPLATE が廃止されたため、G_DEFINE_TYPE に置き換える必要がありました。 gst_cutter_test_runner_init はその影響を受け、引数の定義を変更する必要がありました。

diff --git a/gst-plugins/gst-cutter-test-runner.c b/gst-plugins/gst-cutter-test-runner.c
index f00b0b6..edcc55a 100644
--- a/gst-plugins/gst-cutter-test-runner.c
+++ b/gst-plugins/gst-cutter-test-runner.c
@@ -49,7 +49,7 @@ struct _GstCutterTestRunnerPrivate
     GString *xml_string;
 };
 
-GST_BOILERPLATE(GstCutterTestRunner, gst_cutter_test_runner, GstBaseSrc, GST_TYPE_BASE_SRC);
+G_DEFINE_TYPE(GstCutterTestRunner, gst_cutter_test_runner, GST_TYPE_BASE_SRC);
 
 enum
 {
@@ -129,7 +129,7 @@ gst_cutter_test_runner_class_init (GstCutterTestRunnerClass * klass)
 }
 
 static void
-gst_cutter_test_runner_init (GstCutterTestRunner *cutter_test_runner, GstCutterTestRunnerClass * klass)
+gst_cutter_test_runner_init (GstCutterTestRunner *cutter_test_runner)
 {
     GstCutterTestRunnerPrivate *priv = GST_CUTTER_TEST_RUNNER_GET_PRIVATE(cutter_test_runner);
 

それだけでなく、 _base_init が呼ばれなくなるため、_class_init 内で同等の処理を行う必要がありました。

diff --git a/gst-plugins/gst-cutter-test-runner.c b/gst-plugins/gst-cutter-test-runner.c
index edcc55a..b3f4c99 100644
--- a/gst-plugins/gst-cutter-test-runner.c
+++ b/gst-plugins/gst-cutter-test-runner.c
@@ -80,21 +80,6 @@ static GstStateChangeReturn change_state (GstElement *element,
                                           GstStateChange transition);
 
 static void
-gst_cutter_test_runner_base_init (gpointer klass)
-{
-    GstElementClass *element_class = GST_ELEMENT_CLASS(klass);
-
-    gst_element_class_add_pad_template(element_class,
-        gst_static_pad_template_get(&cutter_test_runner_src_template_factory));
-
-    gst_element_class_set_metadata(element_class,
-                                   "Cutter test runner",
-                                   "Cutter test runner",
-                                   "Cutter test runner",
-                                   "g新部 Hiroyuki Ikezoe  <poincare@ikezoe.net>");
-}
-
-static void
 gst_cutter_test_runner_class_init (GstCutterTestRunnerClass * klass)
 {
     GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
@@ -126,6 +111,15 @@ gst_cutter_test_runner_class_init (GstCutterTestRunnerClass * klass)
     g_type_class_add_private(gobject_class, sizeof(GstCutterTestRunnerPrivate));
 
     GST_DEBUG_CATEGORY_INIT(cutter_test_runner_debug, "cutter-test", 0, "Cutter test elements");
+
+    gst_element_class_add_pad_template(element_class,
+        gst_static_pad_template_get(&cutter_test_runner_src_template_factory));
+
+    gst_element_class_set_metadata(element_class,
+                                   "Cutter test runner",
+                                   "Cutter test runner",
+                                   "Cutter test runner",
+                                   "g新部 Hiroyuki Ikezoe  <poincare@ikezoe.net>");
 }
廃止されたvirtual methodの修正

Cutterでは GstBaseSrc を使っていたことから、check_get_range の廃止に対応する必要がありました。

そのため、元々どういう挙動が期待されていたのかを確認したうえで、同等の振舞いとなるように修正する必要がありました。

元々の振舞いについては、過去のドキュメントを参照しました。

  /* check whether the source would support pull-based operation if
   * it were to be opened now. This vfunc is optional, but should be
   * implemented if possible to avoid unnecessary start/stop cycles.
   * The default implementation will open and close the resource to
   * find out whether get_range is supported and that is usually
   * undesirable. */

元のコードでは、 check_get_range で明示的に FALSE を返しており、上記の説明にあるpullモードはサポートしていません。 GstBaseSrcリファレンスマニュアルを参照すると、pullモードがサポートされる条件が明記されており、is_seekableFALSE を返せばpull モードをサポートしない(従来と挙動が同じになる)ことがわかりました。 たまたま、元のコードでも is_seekableFALSE を返すようになっていたので、最終的には、check_get_rangeがらみのコードを削除するだけでよいことになりました。

diff --git a/gst-plugins/gst-cutter-test-runner.c b/gst-plugins/gst-cutter-test-runner.c
index 9b05c5b..0cedf9d 100644
--- a/gst-plugins/gst-cutter-test-runner.c
+++ b/gst-plugins/gst-cutter-test-runner.c
@@ -75,7 +75,6 @@ static GstFlowReturn create          (GstBaseSrc *basr_src,
                                       guint64     offset,
                                       guint       length,
                                       GstBuffer **buffer);
-static gboolean      check_get_range (GstBaseSrc *base_src);
 
 static GstStateChangeReturn change_state (GstElement *element,
                                           GstStateChange transition);
@@ -100,7 +99,6 @@ gst_cutter_test_runner_class_init (GstCutterTestRunnerClass * klass)
     base_src_class->stop            = stop;
     base_src_class->is_seekable     = is_seekable;
     base_src_class->create          = create;
-    base_src_class->check_get_range = check_get_range;
 
     spec = g_param_spec_string("test-directory",
                                "Test directory",
@@ -248,12 +246,6 @@ is_seekable (GstBaseSrc *base_src)
     return FALSE;
 }
 
-static gboolean
-check_get_range (GstBaseSrc *base_src)
-{
-    return FALSE;
-}
-
 static GstFlowReturn
マクロ定義の変更への追従

従来問題なかったものの、マクロ定義の変更に伴い警告がでるようになってしまった箇所への対応も行いました。 具体的には、プラグインを定義する GST_PLUGIN_DEFINE の引数の変更です。

これまでは、第三引数は文字列を渡すのが正しいやりかたでした。 しかし、文字列化もマクロ展開時に行われるようになったので、それに合わせるようにしました。

diff --git a/gst-plugins/gst-cutter-test.c b/gst-plugins/gst-cutter-test.c
index b373b76..c44ef66 100644
--- a/gst-plugins/gst-cutter-test.c
+++ b/gst-plugins/gst-cutter-test.c
@@ -42,7 +42,7 @@ plugin_init (GstPlugin * plugin)
 }
 
 GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, 
-                   "cutter-test", "Cutter element",
+                   cutter-test, "Cutter element",
                    plugin_init, VERSION, "LGPL",
                    "GstCutterTest", "http://cutter.sf.net");

このようにして、CutterはGstreamer 0.10からGStreamer 1.0系への対応を行いました。

まとめ

今回はCutterというテスティングフレームワークのGStreamer 1.0対応について紹介しました。GStreamer 1.0対応が入ったバージョンをCutter 1.2.5として今月リリースする予定です。

*1 GStreamer 1.0対応は優先度が低かった。

タグ: Cutter
2015-07-17

«前月 最新記事 翌月»
タグ:
年・日ごとに見る
2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|06|07|08|09|10|11|12|
2019|01|02|03|04|05|06|07|08|09|10|11|12|
2020|01|02|03|04|05|06|07|08|09|10|11|12|